From 53f7bf08d37a17e28a549226ba1477ec668d20b5 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 10 Nov 2025 00:19:16 +0100 Subject: [PATCH] remove remaining `CanCreateMixin` --- com/views.py | 2 +- core/auth/mixins.py | 40 --------------------------------- docs/reference/core/auth.md | 1 - docs/tutorial/perms.md | 42 ++++------------------------------ forum/views.py | 45 +++++++++++++++++++++++-------------- trombi/views.py | 22 +++++++++--------- 6 files changed, 45 insertions(+), 107 deletions(-) diff --git a/com/views.py b/com/views.py index eae9bf6a..2d5045d9 100644 --- a/com/views.py +++ b/com/views.py @@ -700,7 +700,7 @@ class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View): parsed = urlparse(referer) if parsed.netloc == settings.SITH_URL: return redirect(parsed.path) - return redirect(reverse("com:poster_list")) + return redirect("com:poster_list") class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView): diff --git a/core/auth/mixins.py b/core/auth/mixins.py index c4e603a9..917200ed 100644 --- a/core/auth/mixins.py +++ b/core/auth/mixins.py @@ -24,7 +24,6 @@ from __future__ import annotations import types -import warnings from typing import TYPE_CHECKING, Any, LiteralString from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin @@ -147,45 +146,6 @@ class GenericContentPermissionMixinBuilder(View): return super().dispatch(request, *arg, **kwargs) -class CanCreateMixin(View): - """Protect any child view that would create an object. - - Raises: - PermissionDenied: - If the user has not the necessary permission - to create the object of the view. - """ - - def __init_subclass__(cls, **kwargs): - warnings.warn( - f"{cls.__name__} is deprecated and should be replaced " - "by other permission verification mecanism.", - DeprecationWarning, - stacklevel=2, - ) - super().__init_subclass__(**kwargs) - - def __init__(self, *args, **kwargs): - warnings.warn( - f"{self.__class__.__name__} is deprecated and should be replaced " - "by other permission verification mecanism.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - def dispatch(self, request, *arg, **kwargs): - if not request.user.is_authenticated: - raise PermissionDenied - return super().dispatch(request, *arg, **kwargs) - - def form_valid(self, form): - obj = form.instance - if can_edit_prop(obj, self.request.user): - return super().form_valid(form) - raise PermissionDenied - - class CanEditPropMixin(GenericContentPermissionMixinBuilder): """Ensure the user has owner permissions on the child view object. diff --git a/docs/reference/core/auth.md b/docs/reference/core/auth.md index 4226bb2c..9dd5928d 100644 --- a/docs/reference/core/auth.md +++ b/docs/reference/core/auth.md @@ -17,7 +17,6 @@ - can_edit_prop - can_edit - can_view - - CanCreateMixin - CanEditMixin - CanViewMixin - CanEditPropMixin diff --git a/docs/tutorial/perms.md b/docs/tutorial/perms.md index 4594c81e..f3e530c4 100644 --- a/docs/tutorial/perms.md +++ b/docs/tutorial/perms.md @@ -212,7 +212,7 @@ Pour les vues sous forme de fonction, il y a le décorateur obj = self.get_object() obj.is_moderated = True obj.save() - return redirect(reverse("com:news_list")) + return redirect("com:news_list") ``` === "Function-based view" @@ -233,7 +233,7 @@ Pour les vues sous forme de fonction, il y a le décorateur news = get_object_or_404(News, id=news_id) news.is_moderated = True news.save() - return redirect(reverse("com:news_list")) + return redirect("com:news_list") ``` ## Accès à des éléments en particulier @@ -447,10 +447,9 @@ l'utilisateur recevra une liste vide d'objet. Voici un exemple d'utilisation en reprenant l'objet Article crée précédemment : ```python -from django.views.generic import CreateView, DetailView - -from core.auth.mixins import CanViewMixin, CanCreateMixin +from django.views.generic import DetailView +from core.auth.mixins import CanViewMixin from com.models import WeekmailArticle @@ -459,48 +458,15 @@ from com.models import WeekmailArticle # d'une classe de base pour fonctionner correctement. class ArticlesDetailView(CanViewMixin, DetailView): model = WeekmailArticle - - -# Même chose pour une vue de création de l'objet Article -class ArticlesCreateView(CanCreateMixin, CreateView): - model = WeekmailArticle ``` Les mixins suivants sont implémentés : -- [CanCreateMixin][core.auth.mixins.CanCreateMixin] : l'utilisateur peut-il créer l'objet ? - Ce mixin existe, mais est déprécié et ne doit plus être utilisé ! - [CanEditPropMixin][core.auth.mixins.CanEditPropMixin] : l'utilisateur peut-il éditer les propriétés de l'objet ? - [CanEditMixin][core.auth.mixins.CanEditMixin] : L'utilisateur peut-il éditer l'objet ? - [CanViewMixin][core.auth.mixins.CanViewMixin] : L'utilisateur peut-il voir l'objet ? - [FormerSubscriberMixin][core.auth.mixins.FormerSubscriberMixin] : L'utilisateur a-t-il déjà été cotisant ? -!!!danger "CanCreateMixin" - - L'usage de `CanCreateMixin` est dangereux et ne doit en aucun cas être - étendu. - La façon dont ce mixin marche est qu'il valide le formulaire - de création et crée l'objet sans le persister en base de données, puis - vérifie les droits sur cet objet non-persisté. - Le danger de ce système vient de multiples raisons : - - - Les vérifications se faisant sur un objet non persisté, - l'utilisation de mécanismes nécessitant une persistance préalable - peut mener à des comportements indésirés, voire à des erreurs. - - Les développeurs de django ayant tendance à restreindre progressivement - les actions qui peuvent être faites sur des objets non-persistés, - les mises-à-jour de django deviennent plus compliquées. - - La vérification des droits ne se fait que dans les requêtes POST, - à la toute fin de la requête. - Tout ce qui arrive avant n'est absolument pas protégé. - Toute opération (même les suppressions et les créations) qui ont - lieu avant la persistance de l'objet seront appliquées, - même sans permission. - - Si un développeur du site fait l'erreur de surcharger - la méthode `form_valid` (ce qui est plutôt courant, - lorsqu'on veut accomplir certaines actions - quand un formulaire est valide), on peut se retrouver - dans une situation où l'objet est persisté sans aucune protection. !!!danger "Performance" diff --git a/forum/views.py b/forum/views.py index 3ce1fa68..3bd786d9 100644 --- a/forum/views.py +++ b/forum/views.py @@ -27,14 +27,14 @@ from functools import partial from django import forms from django.conf import settings -from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import PermissionDenied +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db import IntegrityError from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.utils import html, timezone from django.utils.decorators import method_decorator +from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView, RedirectView from django.views.generic.detail import SingleObjectMixin @@ -44,7 +44,6 @@ from honeypot.decorators import check_honeypot from club.widgets.ajax_select import AutoCompleteSelectClub from core.auth.mixins import ( - CanCreateMixin, CanEditMixin, CanEditPropMixin, CanViewMixin, @@ -180,11 +179,19 @@ class ForumForm(forms.ModelForm): ) -class ForumCreateView(CanCreateMixin, CreateView): +class ForumCreateView(UserPassesTestMixin, CreateView): model = Forum form_class = ForumForm template_name = "core/create.jinja" + def test_func(self): + if self.request.user.has_perm("forum.add_forum"): + return True + parent = Forum.objects.filter(id=self.request.GET["parent"]).first() + if parent is not None: + return self.request.user.is_owner(parent) + return False + def get_initial(self): init = super().get_initial() parent = Forum.objects.filter(id=self.request.GET["parent"]).first() @@ -258,18 +265,19 @@ class TopicForm(forms.ModelForm): @method_decorator( partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post" ) -class ForumTopicCreateView(CanCreateMixin, CreateView): +class ForumTopicCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): model = ForumMessage form_class = TopicForm template_name = "forum/reply.jinja" - def dispatch(self, request, *args, **kwargs): - self.forum = get_object_or_404( - Forum, id=self.kwargs["forum_id"], is_category=False + @cached_property + def forum(self): + return get_object_or_404(Forum, id=self.kwargs["forum_id"], is_category=False) + + def test_func(self): + return self.request.user.has_perm("forum.add_forumtopic") or ( + self.request.user.can_view(self.forum) ) - if not request.user.can_view(self.forum): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) def form_valid(self, form): topic = ForumTopic( @@ -404,7 +412,7 @@ class ForumMessageUndeleteView(SingleObjectMixin, RedirectView): @method_decorator( partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post" ) -class ForumMessageCreateView(CanCreateMixin, CreateView): +class ForumMessageCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): model = ForumMessage form_class = forms.modelform_factory( model=ForumMessage, @@ -413,11 +421,14 @@ class ForumMessageCreateView(CanCreateMixin, CreateView): ) template_name = "forum/reply.jinja" - def dispatch(self, request, *args, **kwargs): - self.topic = get_object_or_404(ForumTopic, id=self.kwargs["topic_id"]) - if not request.user.can_view(self.topic): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) + @cached_property + def topic(self): + return get_object_or_404(ForumTopic, id=self.kwargs["topic_id"]) + + def test_func(self): + return self.request.user.has_perm( + "forum.add_forummessage" + ) or self.request.user.can_view(self.topic) def get_initial(self): init = super().get_initial() diff --git a/trombi/views.py b/trombi/views.py index a1978ec3..3a0d43c8 100644 --- a/trombi/views.py +++ b/trombi/views.py @@ -27,7 +27,7 @@ from datetime import date from django import forms from django.conf import settings from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import PermissionDenied from django.db import IntegrityError @@ -35,17 +35,13 @@ from django.forms.models import modelform_factory from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy +from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, RedirectView, TemplateView, View from django.views.generic.edit import CreateView, DeleteView, UpdateView from club.models import Club -from core.auth.mixins import ( - CanCreateMixin, - CanEditMixin, - CanEditPropMixin, - CanViewMixin, -) +from core.auth.mixins import CanEditMixin, CanEditPropMixin, CanViewMixin from core.models import User from core.views.forms import SelectDate from core.views.mixins import TabedViewMixin @@ -117,19 +113,25 @@ class TrombiForm(forms.ModelForm): widgets = {"subscription_deadline": SelectDate, "comments_deadline": SelectDate} -class TrombiCreateView(CanCreateMixin, CreateView): +class TrombiCreateView(UserPassesTestMixin, CreateView): """Create a trombi for a club.""" model = Trombi form_class = TrombiForm template_name = "core/create.jinja" + @cached_property + def club(self): + return get_object_or_404(Club, id=self.kwargs["club_id"]) + + def test_func(self): + return self.request.user.can_edit(self.club) + def post(self, request, *args, **kwargs): """Affect club.""" form = self.get_form() if form.is_valid(): - club = get_object_or_404(Club, id=self.kwargs["club_id"]) - form.instance.club = club + form.instance.club = self.club ret = self.form_valid(form) return ret else: