feat: Securisation du Backend

This commit is contained in:
Luc SORIGNET
2026-02-27 10:45:36 +01:00
parent 2fef6d61a4
commit fa843097ba
55 changed files with 2898 additions and 910 deletions

View File

@ -0,0 +1,116 @@
"""
Tests de sécurité — GestionEmail
Vérifie :
- search_recipients nécessite une authentification (plus accessible anonymement)
- send-email nécessite une authentification
- Les données personnelles ne sont pas dans les logs INFO
"""
import json
from django.test import TestCase, override_settings
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from Auth.models import Profile, ProfileRole
from Establishment.models import Establishment
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def create_user_with_role(email, password="TestPass!123"):
user = Profile.objects.create_user(
username=email, email=email, password=password
)
est = Establishment.objects.create(
name=f"Ecole {email}",
address="1 rue Test",
total_capacity=50,
establishment_type=[1],
)
ProfileRole.objects.create(
profile=user,
role_type=ProfileRole.RoleType.PROFIL_ECOLE,
establishment=est,
is_active=True,
)
return user, est
OVERRIDE = dict(
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}},
SESSION_ENGINE='django.contrib.sessions.backends.db',
)
# ---------------------------------------------------------------------------
# Tests : search_recipients exige une authentification
# ---------------------------------------------------------------------------
@override_settings(**OVERRIDE)
class SearchRecipientsAuthTest(TestCase):
"""
GET /email/search-recipients/ doit retourner 401 si non authentifié.
Avant la correction, cet endpoint était accessible anonymement
(harvesting d'emails des membres d'un établissement).
"""
def setUp(self):
self.client = APIClient()
self.url = reverse('GestionEmail:search_recipients')
def test_sans_auth_retourne_401(self):
"""Accès anonyme doit être rejeté avec 401."""
response = self.client.get(self.url, {'q': 'test', 'establishment_id': 1})
self.assertEqual(
response.status_code, status.HTTP_401_UNAUTHORIZED,
"search_recipients doit exiger une authentification (OWASP A01 - Broken Access Control)"
)
def test_avec_auth_et_query_vide_retourne_200_ou_liste_vide(self):
"""Un utilisateur authentifié sans terme de recherche reçoit une liste vide."""
user, est = create_user_with_role('search_auth@test.com')
token = str(RefreshToken.for_user(user).access_token)
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
response = self.client.get(self.url, {'q': '', 'establishment_id': est.id})
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_400_BAD_REQUEST])
def test_avec_auth_et_establishment_manquant_retourne_400(self):
"""Un utilisateur authentifié sans establishment_id reçoit 400."""
user, _ = create_user_with_role('search_noest@test.com')
token = str(RefreshToken.for_user(user).access_token)
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
response = self.client.get(self.url, {'q': 'alice'})
self.assertIn(response.status_code, [status.HTTP_400_BAD_REQUEST, status.HTTP_200_OK])
# ---------------------------------------------------------------------------
# Tests : send-email exige une authentification
# ---------------------------------------------------------------------------
@override_settings(**OVERRIDE)
class SendEmailAuthTest(TestCase):
"""
POST /email/send-email/ doit retourner 401 si non authentifié.
"""
def setUp(self):
self.client = APIClient()
self.url = reverse('GestionEmail:send_email')
def test_sans_auth_retourne_401(self):
"""Accès anonyme à l'envoi d'email doit être rejeté."""
payload = {
'recipients': ['victim@example.com'],
'subject': 'Test',
'message': 'Hello',
'establishment_id': 1,
}
response = self.client.post(
self.url, data=json.dumps(payload), content_type='application/json'
)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

View File

@ -2,6 +2,8 @@ from django.http.response import JsonResponse
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q
from Auth.models import Profile, ProfileRole
@ -20,9 +22,11 @@ class SendEmailView(APIView):
"""
API pour envoyer des emails aux parents et professeurs.
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# Ajouter du debug
logger.info(f"Request data received: {request.data}")
logger.info(f"Request data received (keys): {list(request.data.keys()) if request.data else []}") # Ne pas logger les valeurs (RGPD)
logger.info(f"Request content type: {request.content_type}")
data = request.data
@ -34,11 +38,9 @@ class SendEmailView(APIView):
establishment_id = data.get('establishment_id', '')
# Debug des données reçues
logger.info(f"Recipients: {recipients} (type: {type(recipients)})")
logger.info(f"CC: {cc} (type: {type(cc)})")
logger.info(f"BCC: {bcc} (type: {type(bcc)})")
logger.info(f"Recipients (count): {len(recipients)}")
logger.info(f"Subject: {subject}")
logger.info(f"Message length: {len(message) if message else 0}")
logger.debug(f"Message length: {len(message) if message else 0}")
logger.info(f"Establishment ID: {establishment_id}")
if not recipients or not message:
@ -70,12 +72,12 @@ class SendEmailView(APIView):
logger.error(f"NotFound error: {str(e)}")
return Response({'error': str(e)}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"Exception during email sending: {str(e)}")
logger.error(f"Exception type: {type(e)}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
logger.error(f"Exception during email sending: {str(e)}", exc_info=True)
return Response({'error': 'Erreur lors de l\'envoi de l\'email'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def search_recipients(request):
"""
API pour rechercher des destinataires en fonction d'un terme de recherche et d'un établissement.