1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 01:48:21 +02:00

Merge branch 'beta' into migration-django-4-2

This commit is contained in:
korenstin
2024-08-25 16:13:01 +02:00
190 changed files with 2674 additions and 1795 deletions

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'api.apps.APIConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

42
apps/api/filters.py Normal file
View File

@ -0,0 +1,42 @@
import re
from functools import lru_cache
from rest_framework.filters import SearchFilter
class RegexSafeSearchFilter(SearchFilter):
@lru_cache
def validate_regex(self, search_term) -> bool:
try:
re.compile(search_term)
return True
except re.error:
return False
def get_search_fields(self, view, request):
"""
Ensure that given regex are valid.
If not, we consider that the user is trying to search by substring.
"""
search_fields = super().get_search_fields(view, request)
search_terms = self.get_search_terms(request)
for search_term in search_terms:
if not self.validate_regex(search_term):
# Invalid regex. We assume we don't query by regex but by substring.
search_fields = [f.replace('$', '') for f in search_fields]
break
return search_fields
def get_search_terms(self, request):
"""
Ensure that search field is a valid regex query. If not, we remove extra characters.
"""
terms = super().get_search_terms(request)
if not all(self.validate_regex(term) for term in terms):
# Invalid regex. If a ^ is prefixed to the search term, we remove it.
terms = [term[1:] if term[0] == '^' else term for term in terms]
# Same for dollars.
terms = [term[:-1] if term[-1] == '$' else term for term in terms]
return terms

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import json
@ -12,11 +12,12 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models.fields.files import ImageFieldFile
from django.test import TestCase
from django_filters.rest_framework import DjangoFilterBackend
from phonenumbers import PhoneNumber
from rest_framework.filters import OrderingFilter
from api.filters import RegexSafeSearchFilter
from member.models import Membership, Club
from note.models import NoteClub, NoteUser, Alias, Note
from permission.models import PermissionMask, Permission, Role
from phonenumbers import PhoneNumber
from rest_framework.filters import SearchFilter, OrderingFilter
from .viewsets import ContentTypeViewSet, UserViewSet
@ -87,7 +88,7 @@ class TestAPI(TestCase):
resp = self.client.get(url + f"?ordering=-{field}")
self.assertEqual(resp.status_code, 200)
if SearchFilter in backends:
if RegexSafeSearchFilter in backends:
# Basic search
for field in viewset.search_fields:
obj = self.fix_note_object(obj, field)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User

View File

@ -1,19 +1,29 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
from django.conf import settings
from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend
from note.models import Alias
from .filters import RegexSafeSearchFilter
from .serializers import UserSerializer, ContentTypeSerializer
def is_regex(pattern):
try:
re.compile(pattern)
return True
except (re.error, TypeError):
return False
class ReadProtectedModelViewSet(ModelViewSet):
"""
Protect a ModelViewSet by filtering the objects that the user cannot see.
@ -60,34 +70,38 @@ class UserViewSet(ReadProtectedModelViewSet):
if "search" in self.request.GET:
pattern = self.request.GET["search"]
# Check if this is a valid regex. If not, we won't check regex
valid_regex = is_regex(pattern)
suffix = "__iregex" if valid_regex else "__istartswith"
prefix = "^" if valid_regex else ""
# Filter with different rules
# We use union-all to keep each filter rule sorted in result
queryset = queryset.filter(
# Match without normalization
note__alias__name__iregex="^" + pattern
Q(**{f"note__alias__name{suffix}": prefix + pattern})
).union(
queryset.filter(
# Match with normalization
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(note__alias__name__iregex="^" + pattern)
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
).union(
queryset.filter(
# Match on lower pattern
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(note__alias__name__iregex="^" + pattern)
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
).union(
queryset.filter(
# Match on firstname or lastname
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
& ~Q(note__alias__name__iregex="^" + pattern)
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
),
all=True,
)
@ -107,6 +121,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
"""
queryset = ContentType.objects.order_by('id')
serializer_class = ContentTypeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['id', 'app_label', 'model', ]
search_fields = ['$app_label', '$model', ]