First commit
This commit is contained in:
0
profiles/__init__.py
Normal file
0
profiles/__init__.py
Normal file
10
profiles/admin.py
Normal file
10
profiles/admin.py
Normal 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
10
profiles/apps.py
Normal 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
|
||||
24
profiles/migrations/0001_initial.py
Normal file
24
profiles/migrations/0001_initial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
profiles/migrations/__init__.py
Normal file
0
profiles/migrations/__init__.py
Normal file
22
profiles/models.py
Normal file
22
profiles/models.py
Normal 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
20
profiles/signals.py
Normal 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
20
profiles/urls.py
Normal 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
59
profiles/views.py
Normal 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é")
|
||||
|
||||
Reference in New Issue
Block a user