1 Commits

Author SHA1 Message Date
dependabot[bot]
b927b9c0f2 [UPDATE] Update pytest requirement
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.2...9.0.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 08:06:35 +00:00
7 changed files with 108 additions and 46 deletions

View File

@@ -700,7 +700,7 @@ class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View):
parsed = urlparse(referer) parsed = urlparse(referer)
if parsed.netloc == settings.SITH_URL: if parsed.netloc == settings.SITH_URL:
return redirect(parsed.path) return redirect(parsed.path)
return redirect("com:poster_list") return redirect(reverse("com:poster_list"))
class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView): class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView):

View File

@@ -24,6 +24,7 @@
from __future__ import annotations from __future__ import annotations
import types import types
import warnings
from typing import TYPE_CHECKING, Any, LiteralString from typing import TYPE_CHECKING, Any, LiteralString
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
@@ -146,6 +147,45 @@ class GenericContentPermissionMixinBuilder(View):
return super().dispatch(request, *arg, **kwargs) 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): class CanEditPropMixin(GenericContentPermissionMixinBuilder):
"""Ensure the user has owner permissions on the child view object. """Ensure the user has owner permissions on the child view object.

View File

@@ -17,6 +17,7 @@
- can_edit_prop - can_edit_prop
- can_edit - can_edit
- can_view - can_view
- CanCreateMixin
- CanEditMixin - CanEditMixin
- CanViewMixin - CanViewMixin
- CanEditPropMixin - CanEditPropMixin

View File

@@ -212,7 +212,7 @@ Pour les vues sous forme de fonction, il y a le décorateur
obj = self.get_object() obj = self.get_object()
obj.is_moderated = True obj.is_moderated = True
obj.save() obj.save()
return redirect("com:news_list") return redirect(reverse("com:news_list"))
``` ```
=== "Function-based view" === "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 = get_object_or_404(News, id=news_id)
news.is_moderated = True news.is_moderated = True
news.save() news.save()
return redirect("com:news_list") return redirect(reverse("com:news_list"))
``` ```
## Accès à des éléments en particulier ## Accès à des éléments en particulier
@@ -447,9 +447,10 @@ l'utilisateur recevra une liste vide d'objet.
Voici un exemple d'utilisation en reprenant l'objet Article crée précédemment : Voici un exemple d'utilisation en reprenant l'objet Article crée précédemment :
```python ```python
from django.views.generic import DetailView from django.views.generic import CreateView, DetailView
from core.auth.mixins import CanViewMixin, CanCreateMixin
from core.auth.mixins import CanViewMixin
from com.models import WeekmailArticle from com.models import WeekmailArticle
@@ -458,15 +459,48 @@ from com.models import WeekmailArticle
# d'une classe de base pour fonctionner correctement. # d'une classe de base pour fonctionner correctement.
class ArticlesDetailView(CanViewMixin, DetailView): class ArticlesDetailView(CanViewMixin, DetailView):
model = WeekmailArticle 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 : 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 ? - [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 ? - [CanEditMixin][core.auth.mixins.CanEditMixin] : L'utilisateur peut-il éditer l'objet ?
- [CanViewMixin][core.auth.mixins.CanViewMixin] : L'utilisateur peut-il voir 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 ? - [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" !!!danger "Performance"

View File

@@ -27,14 +27,14 @@ from functools import partial
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db import IntegrityError from django.db import IntegrityError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import html, timezone from django.utils import html, timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, RedirectView from django.views.generic import DetailView, ListView, RedirectView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
@@ -44,6 +44,7 @@ from honeypot.decorators import check_honeypot
from club.widgets.ajax_select import AutoCompleteSelectClub from club.widgets.ajax_select import AutoCompleteSelectClub
from core.auth.mixins import ( from core.auth.mixins import (
CanCreateMixin,
CanEditMixin, CanEditMixin,
CanEditPropMixin, CanEditPropMixin,
CanViewMixin, CanViewMixin,
@@ -179,19 +180,11 @@ class ForumForm(forms.ModelForm):
) )
class ForumCreateView(UserPassesTestMixin, CreateView): class ForumCreateView(CanCreateMixin, CreateView):
model = Forum model = Forum
form_class = ForumForm form_class = ForumForm
template_name = "core/create.jinja" 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): def get_initial(self):
init = super().get_initial() init = super().get_initial()
parent = Forum.objects.filter(id=self.request.GET["parent"]).first() parent = Forum.objects.filter(id=self.request.GET["parent"]).first()
@@ -265,19 +258,18 @@ class TopicForm(forms.ModelForm):
@method_decorator( @method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post" partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
) )
class ForumTopicCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): class ForumTopicCreateView(CanCreateMixin, CreateView):
model = ForumMessage model = ForumMessage
form_class = TopicForm form_class = TopicForm
template_name = "forum/reply.jinja" template_name = "forum/reply.jinja"
@cached_property def dispatch(self, request, *args, **kwargs):
def forum(self): self.forum = get_object_or_404(
return get_object_or_404(Forum, id=self.kwargs["forum_id"], is_category=False) 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): def form_valid(self, form):
topic = ForumTopic( topic = ForumTopic(
@@ -412,7 +404,7 @@ class ForumMessageUndeleteView(SingleObjectMixin, RedirectView):
@method_decorator( @method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post" partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
) )
class ForumMessageCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): class ForumMessageCreateView(CanCreateMixin, CreateView):
model = ForumMessage model = ForumMessage
form_class = forms.modelform_factory( form_class = forms.modelform_factory(
model=ForumMessage, model=ForumMessage,
@@ -421,14 +413,11 @@ class ForumMessageCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView
) )
template_name = "forum/reply.jinja" template_name = "forum/reply.jinja"
@cached_property def dispatch(self, request, *args, **kwargs):
def topic(self): self.topic = get_object_or_404(ForumTopic, id=self.kwargs["topic_id"])
return get_object_or_404(ForumTopic, id=self.kwargs["topic_id"]) if not request.user.can_view(self.topic):
raise PermissionDenied
def test_func(self): return super().dispatch(request, *args, **kwargs)
return self.request.user.has_perm(
"forum.add_forummessage"
) or self.request.user.can_view(self.topic)
def get_initial(self): def get_initial(self):
init = super().get_initial() init = super().get_initial()

View File

@@ -73,7 +73,7 @@ dev = [
] ]
tests = [ tests = [
"freezegun>=1.5.5,<2.0.0", "freezegun>=1.5.5,<2.0.0",
"pytest>=8.4.2,<9.0.0", "pytest>=8.4.2,<10.0.0",
"pytest-cov>=7.0.0,<8.0.0", "pytest-cov>=7.0.0,<8.0.0",
"pytest-django<5.0.0,>=4.10.0", "pytest-django<5.0.0,>=4.10.0",
"model-bakery<2.0.0,>=1.20.4", "model-bakery<2.0.0,>=1.20.4",

View File

@@ -27,7 +27,7 @@ from datetime import date
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import IntegrityError from django.db import IntegrityError
@@ -35,13 +35,17 @@ from django.forms.models import modelform_factory
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, RedirectView, TemplateView, View from django.views.generic import DetailView, RedirectView, TemplateView, View
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from club.models import Club from club.models import Club
from core.auth.mixins import CanEditMixin, CanEditPropMixin, CanViewMixin from core.auth.mixins import (
CanCreateMixin,
CanEditMixin,
CanEditPropMixin,
CanViewMixin,
)
from core.models import User from core.models import User
from core.views.forms import SelectDate from core.views.forms import SelectDate
from core.views.mixins import TabedViewMixin from core.views.mixins import TabedViewMixin
@@ -113,25 +117,19 @@ class TrombiForm(forms.ModelForm):
widgets = {"subscription_deadline": SelectDate, "comments_deadline": SelectDate} widgets = {"subscription_deadline": SelectDate, "comments_deadline": SelectDate}
class TrombiCreateView(UserPassesTestMixin, CreateView): class TrombiCreateView(CanCreateMixin, CreateView):
"""Create a trombi for a club.""" """Create a trombi for a club."""
model = Trombi model = Trombi
form_class = TrombiForm form_class = TrombiForm
template_name = "core/create.jinja" 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): def post(self, request, *args, **kwargs):
"""Affect club.""" """Affect club."""
form = self.get_form() form = self.get_form()
if form.is_valid(): if form.is_valid():
form.instance.club = self.club club = get_object_or_404(Club, id=self.kwargs["club_id"])
form.instance.club = club
ret = self.form_valid(form) ret = self.form_valid(form)
return ret return ret
else: else: