First commit

This commit is contained in:
Johan
2025-12-18 14:35:15 +01:00
commit 460c962bb6
26 changed files with 796 additions and 0 deletions

0
profiles/__init__.py Normal file
View File

10
profiles/admin.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import Profile
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
"""
Configuration de l'interface admin pour les profils.
"""
list_display = ('user', 'secret_note')
search_fields = ('user__username',)

10
profiles/apps.py Normal file
View File

@@ -0,0 +1,10 @@
from django.apps import AppConfig
class ProfilesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'profiles'
def ready(self):
# Importer les signaux pour qu'ils soient enregistrés
import profiles.signals

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.2.8 on 2025-11-09 16:32
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('secret_note', models.CharField(blank=True, help_text='Un secret que seul cet utilisateur devrait voir.', max_length=255, verbose_name='Note secrète')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

22
profiles/models.py Normal file
View File

@@ -0,0 +1,22 @@
from django.db import models
from django.conf import settings
class Profile(models.Model):
"""
Modèle de profil simple lié à chaque utilisateur.
"""
# Relation 1-pour-1 avec le modèle User de Django
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
# Le champ "sensible" que nous voulons protéger
secret_note = models.CharField(
"Note secrète",
max_length=255,
blank=True,
help_text="Un secret que seul cet utilisateur devrait voir."
)
def __str__(self):
return f"Profil de {self.user.username}"

20
profiles/signals.py Normal file
View File

@@ -0,0 +1,20 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
from .models import Profile
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_user_profile(sender, instance, created, **kwargs):
"""
Signal pour créer automatiquement un objet Profile
chaque fois qu'un nouvel utilisateur (User) est créé.
"""
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def save_user_profile(sender, instance, **kwargs):
"""
Signal pour sauvegarder le profil lorsque l'utilisateur est sauvegardé.
"""
instance.profile.save()

20
profiles/urls.py Normal file
View File

@@ -0,0 +1,20 @@
from django.urls import path
from . import views
app_name = 'profiles'
urlpatterns = [
# L'URL VULNÉRABLE
path(
'vulnerable/<int:pk>/',
views.VulnerableProfileView.as_view(),
name='vulnerable_detail'
),
# L'URL SÉCURISÉE
path(
'secure/<int:pk>/',
views.SecureProfileView.as_view(),
name='secure_detail'
),
]

59
profiles/views.py Normal file
View File

@@ -0,0 +1,59 @@
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.shortcuts import render
from django.views.generic import DetailView
from .models import Profile
#
# 1. LA VUE VULNÉRABLE (IDOR)
#
class VulnerableProfileView(LoginRequiredMixin, DetailView):
"""
Cette vue est VULNÉRABLE au Broken Access Control (IDOR).
- LoginRequiredMixin: Vérifie si l'utilisateur est connecté.
- DetailView: Récupère un objet par sa clé primaire (pk) passée dans l'URL.
PROBLÈME: Elle ne vérifie jamais si l'utilisateur connecté
est le *propriétaire* du profil qu'il demande.
"""
model = Profile
template_name = 'profiles/profile_detail.html'
context_object_name = 'profile' # Nom de l'objet dans le template
#
# 2. LA VUE SÉCURISÉE (CORRIGÉE)
#
class SecureProfileView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
"""
Cette vue est CORRIGÉE et protège contre l'IDOR.
- UserPassesTestMixin: Ajoute un test de permission personnalisé.
La vue n'est rendue que si la méthode `test_func` renvoie True.
Sinon, elle renvoie un "403 Forbidden".
"""
model = Profile
template_name = 'profiles/profile_detail.html'
context_object_name = 'profile'
def test_func(self):
"""
La fonction de test pour UserPassesTestMixin.
C'est le cœur de la correction.
"""
# 1. Récupère l'objet Profile que DetailView a trouvé (via self.get_object())
profile_to_view = self.get_object()
# 2. Vérifie si l'utilisateur de la requête (connecté)
# est le même que l'utilisateur lié à ce profil.
is_owner = (self.request.user == profile_to_view.user)
return is_owner
# Optionnel: Si vous préférez renvoyer un 404 (Not Found) au lieu d'un 403 (Forbidden)
# pour cacher l'existence de l'objet, vous pouvez surcharger handle_no_permission.
# def handle_no_permission(self):
# from django.http import Http404
# raise Http404("Profil non trouvé")