1 / 12

🔐 Sécurisation des routes Symfony 6.4

Les ACL (Access Control Lists)

Formation DWWM - Développeur Web et Web Mobile

Comprendre et implémenter la sécurité dans vos applications Symfony

🎯 Objectifs de cette formation :

  • Comprendre les concepts de sécurité web
  • Maîtriser le système de sécurité Symfony
  • Implémenter des ACL efficaces
  • Sécuriser les routes de votre application

🌐 Pourquoi sécuriser ses routes ?

Les risques sans sécurité

⚠️ Dangers potentiels :

  • Accès non autorisé : utilisateurs malveillants dans l'admin
  • Manipulation de données : modification/suppression par des tiers
  • Vol d'informations : données personnelles exposées
  • Défacement : modification du contenu du site

La sécurité par couches

🏗️ Architecture de sécurité :

1. Authentification : "Qui êtes-vous ?" (login/mot de passe)

2. Autorisation : "Que pouvez-vous faire ?" (rôles et permissions)

3. Contrôle d'accès : "À quoi pouvez-vous accéder ?" (ACL)

💡 Bon à savoir : Symfony fournit un système de sécurité robuste et flexible qui couvre tous ces aspects !

🛡️ Le système de sécurité Symfony

Vue d'ensemble

Symfony utilise un système de sécurité basé sur plusieurs composants qui travaillent ensemble :

Composant Rôle Exemple
Firewall Point d'entrée de la sécurité Configuration dans security.yaml
User Provider Récupère les utilisateurs Base de données, LDAP, mémoire
Encoder Chiffre les mots de passe bcrypt, sodium
Access Control Règles d'accès aux routes ROLE_USER, ROLE_ADMIN

Configuration de base

config/packages/security.yaml
security:
password_hashers:
        App\Entity\User:
            algorithm: auto
    
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    
    firewalls:
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
            logout:
                path: app_logout

📋 Qu'est-ce qu'une ACL ?

Définition

ACL (Access Control List) = Liste de contrôle d'accès

Une ACL définit qui peut faire quoi sur quelle ressource.

Différence avec les rôles simples

🎭 Rôles simples (ROLE_ADMIN, ROLE_USER) :

  • Permissions globales
  • Soit tout, soit rien
  • Exemple : ROLE_ADMIN peut tout modifier

🎯 ACL (granulaires) :

  • Permissions spécifiques par objet
  • Contrôle fin des accès
  • Exemple : Jean peut modifier l'article #5 mais pas l'article #10

Cas d'usage concrets

🏢 Exemples métier :

  • Blog : Un auteur ne peut modifier que ses propres articles
  • E-commerce : Un client ne voit que ses commandes
  • CRM : Un commercial ne gère que ses prospects
  • Projet : Les membres n'accèdent qu'aux projets assignés

🔧 Types de contrôle d'accès dans Symfony

1. Access Control (security.yaml)

✅ Avantages :

  • Simple à configurer
  • Basé sur les URLs
  • Parfait pour des règles globales

❌ Inconvénients :

  • Pas de logique métier
  • Pas de contrôle par objet
access_control:
    - { path: ^/admin, roles: ROLE_ADMIN }
    - { path: ^/profile, roles: ROLE_USER }

2. Annotations/Attributs dans les contrôleurs

✅ Avantages :

  • Proche du code métier
  • Logique conditionnelle possible
  • Contrôle par action
#[IsGranted('ROLE_ADMIN')]
public function adminDashboard(): Response
{
    // Code du tableau de bord admin
}

3. Voters (ACL personnalisées)

✅ Avantages :

  • Logique métier complexe
  • Contrôle par objet spécifique
  • Réutilisable
  • Testable

⚙️ Installation et configuration

Prérequis

  • Symfony 6.4 installé
  • Système d'authentification fonctionnel
  • Entité User avec rôles

1. Installation du composant Security

composer require symfony/security-bundle

2. Génération de l'entité User

php bin/console make:user

3. Configuration de base

config/packages/security.yaml
security:
password_hashers:
        App\Entity\User:
            algorithm: auto
    
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout
                target: app_login
    
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/profile, roles: ROLE_USER }

🗳️ Création d'un Voter (ACL personnalisée)

Qu'est-ce qu'un Voter ?

Un Voter est une classe qui décide si un utilisateur a le droit d'effectuer une action sur un objet spécifique.

Génération automatique

php bin/console make:voter

Structure d'un Voter

src/Security/ArticleVoter.php
<?php
namespace App\Security;

use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    public const EDIT = 'ARTICLE_EDIT';
    public const VIEW = 'ARTICLE_VIEW';
    public const DELETE = 'ARTICLE_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::VIEW, self::DELETE])
            && $subject instanceof Article;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        
        if (!$user instanceof User) {
            return false;
        }

        /** @var Article $article */
        $article = $subject;

        return match($attribute) {
            self::VIEW => $this->canView($article, $user),
            self::EDIT => $this->canEdit($article, $user),
            self::DELETE => $this->canDelete($article, $user),
            default => false,
        };
    }

🔨 Implémentation des méthodes du Voter

Méthodes de contrôle d'accès

src/Security/ArticleVoter.php (suite)
private function canView(Article $article, User $user): bool {
// Un article publié peut être vu par tous les utilisateurs connectés
        if ($article->isPublished()) {
            return true;
        }
        
        // Un article non publié ne peut être vu que par son auteur ou un admin
        return $this->canEdit($article, $user);
    }

    private function canEdit(Article $article, User $user): bool
    {
        // L'auteur peut toujours modifier son article
        if ($article->getAuthor() === $user) {
            return true;
        }
        
        // Un admin peut modifier tous les articles
        return in_array('ROLE_ADMIN', $user->getRoles());
    }

    private function canDelete(Article $article, User $user): bool
    {
        // Seul un admin peut supprimer des articles
        return in_array('ROLE_ADMIN', $user->getRoles());
    }
}

💡 Points clés :

  • canView() : Logique pour afficher un article
  • canEdit() : Vérification propriétaire ou admin
  • canDelete() : Restriction aux admins uniquement
  • Réutilisabilité : canView() utilise canEdit()

🎮 Utilisation dans les contrôleurs

Méthode 1 : Attribut IsGranted

src/Controller/ArticleController.php
<?php
namespace App\Controller;

use App\Entity\Article;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class ArticleController extends AbstractController
{
    #[Route('/article/{id}', name: 'article_show')]
    #[IsGranted('ARTICLE_VIEW', subject: 'article')]
    public function show(Article $article): Response
    {
        return $this->render('article/show.html.twig', [
            'article' => $article,
        ]);
    }
    
    #[Route('/article/{id}/edit', name: 'article_edit')]
    #[IsGranted('ARTICLE_EDIT', subject: 'article')]
    public function edit(Article $article): Response
    {
        // Logique d'édition
        return $this->render('article/edit.html.twig', [
            'article' => $article,
        ]);
    }
}

⚠️ Important :

L'attribut subject: 'article' fait référence au paramètre de la méthode qui contient l'objet à tester.

💻 Méthode programmatique

Utilisation de denyAccessUnlessGranted()

src/Controller/ArticleController.php
#[Route('/article/{id}/delete', name: 'article_delete', methods: ['POST'])]
    public function delete(Article $article): Response
    {
        // Vérification des permissions
        $this->denyAccessUnlessGranted('ARTICLE_DELETE', $article);
        
        // Logique de suppression
        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->remove($article);
        $entityManager->flush();
        
        $this->addFlash('success', 'Article supprimé avec succès.');
        
        return $this->redirectToRoute('article_list');
    }

Vérification conditionnelle avec isGranted()

#[Route('/article/{id}/admin-panel', name: 'article_admin_panel')]
    public function adminPanel(Article $article): Response
    {
        $canEdit = $this->isGranted('ARTICLE_EDIT', $article);
        $canDelete = $this->isGranted('ARTICLE_DELETE', $article);
        
        return $this->render('article/admin_panel.html.twig', [
            'article' => $article,
            'can_edit' => $canEdit,
            'can_delete' => $canDelete,
        ]);
    }

🔧 Différences d'usage :

  • denyAccessUnlessGranted() : Bloque et lève une exception
  • isGranted() : Retourne true/false pour la logique conditionnelle

🎨 Utilisation dans les templates Twig

Fonction is_granted()

templates/article/show.html.twig
{% extends 'base.html.twig' %}

{% block title %}{{ article.title }}{% endblock %}

{% block body %}
    <div class="article">
        <h1>{{ article.title }}</h1>
        <p class="author">Par {{ article.author.name }}</p>
        
        <div class="content">
            {{ article.content|nl2br }}
        </div>
        
        <div class="actions">
            {% if is_granted('ARTICLE_EDIT', article) %}
                <a href="{{ path('article_edit', {id: article.id}) }}" 
                   class="btn btn-primary">
                    ✏️ Modifier
                </a>
            {% endif %}
            
            {% if is_granted('ARTICLE_DELETE', article) %}
                <form method="post" 
                      action="{{ path('article_delete', {id: article.id}) }}" 
                      style="display: inline;">
                    <button type="submit" 
                            class="btn btn-danger"
                            onclick="return confirm('Êtes-vous sûr ?')">
                        🗑️ Supprimer
                    </button>
                </form>
            {% endif %}
        </div>
    </div>
{% endblock %}

Menu conditionnel

templates/partials/navigation.html.twig
<nav class="navbar">
    <ul>
        <li><a href="{{ path('home') }}">Accueil</a></li>
        
        {% if is_granted('ROLE_USER') %}
            <li><a href="{{ path('article_list') }}">Articles</a></li>
            <li><a href="{{ path('profile') }}">Mon profil</a></li>
        {% endif %}
        
        {% if is_granted('ROLE_ADMIN') %}
            <li><a href="{{ path('admin_dashboard') }}">Administration</a></li>
        {% endif %}
    </ul>
</nav>

✅ Bonnes pratiques et conclusion

🎯 Bonnes pratiques

1. Organisation des permissions :

  • Définir des constantes pour les permissions (ARTICLE_EDIT)
  • Grouper les permissions logiques dans le même Voter
  • Utiliser des noms explicites et cohérents

2. Sécurité défensive :

  • Vérifier les permissions côté serveur ET côté client
  • Ne jamais faire confiance aux données du front-end
  • Préférer "deny by default" (refuser par défaut)

3. Performance :

  • Éviter les requêtes N+1 dans les Voters
  • Mettre en cache les résultats coûteux
  • Utiliser les jointures dans les requêtes Doctrine

⚠️ Erreurs courantes à éviter :

  • Oublier les vérifications côté serveur : Les ACL front-end ne suffisent pas
  • Permissions trop larges : Principe du moindre privilège
  • Logique dupliquée : Centraliser dans les Voters
  • Tests insuffisants : Tester tous les cas d'usage

📚 Pour aller plus loin

🚀 Fonctionnalités avancées :

  • Hierarchie de rôles : ROLE_ADMIN hérite de ROLE_USER
  • Expressions de sécurité : Logique complexe dans les annotations
  • Event Listeners : Personnaliser le comportement d'authentification
  • API Platform : ACL pour les APIs REST

🎓 Récapitulatif

Ce que vous maîtrisez maintenant :

  • ✅ Concepts de base de la sécurité web
  • ✅ Configuration du système de sécurité Symfony
  • ✅ Création et utilisation des Voters
  • ✅ Sécurisation des routes et des templates
  • ✅ Bonnes pratiques de sécurité

🎉 Bravo ! Vous êtes prêts à sécuriser vos applications Symfony !

🏗️ Exemple pratique complet

Cas d'usage : Blog avec plusieurs auteurs

📋 Règles métier :

  • Tout utilisateur connecté peut voir les articles publiés
  • Un auteur peut voir, créer et modifier ses propres articles
  • Un éditeur peut modifier tous les articles
  • Un admin peut tout faire (y compris supprimer)

1. Entité Article

src/Entity/Article.php
<?php

namespace App\Entity;

use App\Entity\User;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $title = null;

    #[ORM\Column(type: 'text')]
    private ?string $content = null;

    #[ORM\Column]
    private ?bool $published = false;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?User $author = null;

    #[ORM\Column]
    private ?\DateTimeImmutable $createdAt = null;

    // Getters et setters...
    
    public function isPublished(): bool
    {
        return $this->published ?? false;
    }
}

🗳️ Voter avancé avec gestion des rôles

src/Security/ArticleVoter.php
<?php

namespace App\Security;

use App\Entity\Article;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ArticleVoter extends Voter
{
    public const VIEW = 'ARTICLE_VIEW';
    public const CREATE = 'ARTICLE_CREATE';
    public const EDIT = 'ARTICLE_EDIT';
    public const DELETE = 'ARTICLE_DELETE';
    public const PUBLISH = 'ARTICLE_PUBLISH';

    protected function supports(string $attribute, mixed $subject): bool
    {
        if ($attribute === self::CREATE) {
            return true; // CREATE ne nécessite pas d'objet
        }
        
        return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE, self::PUBLISH])
            && $subject instanceof Article;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        
        if (!$user instanceof User) {
            return false;
        }

        return match($attribute) {
            self::CREATE => $this->canCreate($user),
            self::VIEW => $this->canView($subject, $user),
            self::EDIT => $this->canEdit($subject, $user),
            self::DELETE => $this->canDelete($subject, $user),
            self::PUBLISH => $this->canPublish($subject, $user),
            default => false,
        };
    }

    private function canCreate(User $user): bool
    {
        // Tout utilisateur connecté peut créer un article
        return true;
    }

    private function canView(Article $article, User $user): bool
    {
        // Article publié = visible par tous les utilisateurs connectés
        if ($article->isPublished()) {
            return true;
        }
        
        // Article non publié = visible par l'auteur, éditeur ou admin
        return $this->isOwner($article, $user) 
            || $this->hasRole($user, 'ROLE_EDITOR')
            || $this->hasRole($user, 'ROLE_ADMIN');
    }

    private function canEdit(Article $article, User $user): bool
    {
        // L'auteur peut modifier son article
        if ($this->isOwner($article, $user)) {
            return true;
        }
        
        // Un éditeur ou admin peut modifier tous les articles
        return $this->hasRole($user, 'ROLE_EDITOR') 
            || $this->hasRole($user, 'ROLE_ADMIN');
    }

    private function canDelete(Article $article, User $user): bool
    {
        // Seul un admin peut supprimer
        return $this->hasRole($user, 'ROLE_ADMIN');
    }

    private function canPublish(Article $article, User $user): bool
    {
        // Un éditeur ou admin peut publier
        return $this->hasRole($user, 'ROLE_EDITOR') 
            || $this->hasRole($user, 'ROLE_ADMIN');
    }

    private function isOwner(Article $article, User $user): bool
    {
        return $article->getAuthor() === $user;
    }

    private function hasRole(User $user, string $role): bool
    {
        return in_array($role, $user->getRoles());
    }
}

🧪 Tests unitaires des ACL

Pourquoi tester les ACL ?

⚠️ Risques sans tests :

  • Failles de sécurité non détectées
  • Régressions lors des modifications
  • Comportements imprévisibles

Test du Voter

tests/Security/ArticleVoterTest.php
<?php
namespace App\Tests\Security;

use App\Entity\Article;
use App\Entity\User;
use App\Security\ArticleVoter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class ArticleVoterTest extends TestCase
{
    private ArticleVoter $voter;

    protected function setUp(): void
    {
        $this->voter = new ArticleVoter();
    }

    public function testUserCanViewPublishedArticle(): void
    {
        $user = $this->createUser(['ROLE_USER']);
        $article = $this->createArticle($this->createUser(), true);
        $token = $this->createToken($user);

        $result = $this->voter->vote($token, $article, ['ARTICLE_VIEW']);

        $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result);
    }

    public function testUserCannotViewUnpublishedArticleOfOtherUser(): void
    {
        $user = $this->createUser(['ROLE_USER']);
        $otherUser = $this->createUser(['ROLE_USER']);
        $article = $this->createArticle($otherUser, false);
        $token = $this->createToken($user);

        $result = $this->voter->vote($token, $article, ['ARTICLE_VIEW']);

        $this->assertEquals(VoterInterface::ACCESS_DENIED, $result);
    }

    public function testAuthorCanEditOwnArticle(): void
    {
        $user = $this->createUser(['ROLE_USER']);
        $article = $this->createArticle($user, false);
        $token = $this->createToken($user);

        $result = $this->voter->vote($token, $article, ['ARTICLE_EDIT']);

        $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result);
    }

    public function testEditorCanEditAnyArticle(): void
    {
        $editor = $this->createUser(['ROLE_EDITOR']);
        $author = $this->createUser(['ROLE_USER']);
        $article = $this->createArticle($author, false);
        $token = $this->createToken($editor);

        $result = $this->voter->vote($token, $article, ['ARTICLE_EDIT']);

        $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result);
    }

    public function testOnlyAdminCanDelete(): void
    {
        $admin = $this->createUser(['ROLE_ADMIN']);
        $user = $this->createUser(['ROLE_USER']);
        $article = $this->createArticle($user, true);
        
        // Test admin peut supprimer
        $adminToken = $this->createToken($admin);
        $result = $this->voter->vote($adminToken, $article, ['ARTICLE_DELETE']);
        $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result);
        
        // Test utilisateur normal ne peut pas supprimer
        $userToken = $this->createToken($user);
        $result = $this->voter->vote($userToken, $article, ['ARTICLE_DELETE']);
        $this->assertEquals(VoterInterface::ACCESS_DENIED, $result);
    }

    private function createUser(array $roles = []): User
    {
        $user = new User();
        $user->setRoles($roles);
        return $user;
    }

    private function createArticle(User $author, bool $published = false): Article
    {
        $article = new Article();
        $article->setAuthor($author);
        $article->setPublished($published);
        return $article;
    }

    private function createToken(User $user): TokenInterface
    {
        $token = $this->createMock(TokenInterface::class);
        $token->method('getUser')->willReturn($user);
        return $token;
    }
}

🔥 Expressions de sécurité avancées

Qu'est-ce qu'une expression de sécurité ?

Les expressions de sécurité permettent d'écrire des règles complexes directement dans les annotations/attributs.

Syntaxe et variables disponibles

🔧 Variables automatiques :

  • user : L'utilisateur connecté
  • object : L'objet passé en paramètre
  • subject : Alias pour object
  • token : Le token de sécurité
  • roles : Les rôles de l'utilisateur

Exemples d'expressions

src/Controller/ArticleController.php
use Symfony\Component\Security\Http\Attribute\IsGranted;
class ArticleController extends AbstractController
{
    // Expression simple : vérifier le propriétaire
    #[Route('/article/{id}/edit')]
    #[IsGranted("user === object.getAuthor()")]
    public function edit(Article $object): Response { }

    // Expression avec OR : propriétaire OU admin
    #[Route('/article/{id}/moderate')]
    #[IsGranted("user === object.getAuthor() or is_granted('ROLE_ADMIN')")]
    public function moderate(Article $object): Response { }

    // Expression avec méthodes : article publié ET utilisateur connecté
    #[Route('/article/{id}/comment')]
    #[IsGranted("object.isPublished() and is_granted('ROLE_USER')")]
    public function addComment(Article $object): Response { }

    // Expression complexe : éditeur dans la même catégorie
    #[Route('/article/{id}/advanced-edit')]
    #[IsGranted("is_granted('ROLE_EDITOR') and user.getCategory() === object.getCategory()")]
    public function advancedEdit(Article $object): Response { }
}

⚠️ Attention :

  • Les expressions sont évaluées à chaque requête
  • Éviter les appels coûteux dans les expressions
  • Pour des logiques complexes, préférer les Voters

🏗️ Hiérarchie des rôles

Principe

La hiérarchie des rôles permet de définir que certains rôles héritent automatiquement des permissions d'autres rôles.

Configuration

config/packages/security.yaml
security:
role_hierarchy:
        ROLE_AUTHOR: ROLE_USER
        ROLE_EDITOR: [ROLE_AUTHOR, ROLE_USER]
        ROLE_ADMIN: [ROLE_EDITOR, ROLE_AUTHOR, ROLE_USER]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

Schéma visuel

📊 Hiérarchie :

ROLE_SUPER_ADMIN
  └── ROLE_ADMIN
      └── ROLE_EDITOR
          └── ROLE_AUTHOR
              └── ROLE_USER

Impact sur les Voters

Voter simplifié grâce à la hiérarchie
private function canEdit(Article $article, User $user): bool
{
    // Grâce à la hiérarchie, pas besoin de vérifier ROLE_ADMIN
    // car ROLE_EDITOR l'inclut automatiquement
    if ($this->isOwner($article, $user)) {
        return true;
    }
    
    // Un éditeur peut modifier tous les articles
    return $this->hasRole($user, 'ROLE_EDITOR');
}

✅ Avantages :

  • Code plus simple et lisible
  • Moins de duplication
  • Gestion centralisée des permissions
  • Évolutivité facilitée

🔍 Debugging et outils de développement

Commandes utiles

# Vérifier la configuration de sécurité
php bin/console debug:config security

# Lister tous les voters
php bin/console debug:container --tag=security.voter

# Analyser les routes sécurisées
php bin/console debug:router --show-controllers

# Vérifier les rôles d'un utilisateur
php bin/console security:check-roles ROLE_ADMIN

Profiler Symfony

🛠️ Section Security du Profiler :

  • Utilisateur connecté et ses rôles
  • Firewall utilisé
  • Voters exécutés
  • Décisions d'accès
  • Temps d'exécution

Logs de sécurité

config/packages/dev/monolog.yaml
monolog:
handlers:
        security:
            type: stream
            path: '%kernel.logs_dir%/security.log'
            level: debug
            channels: ['security']

Debug dans les Voters

// Dans votre Voter
use Psr\Log\LoggerInterface;

class ArticleVoter extends Voter
{
    public function __construct(private LoggerInterface $logger)
    {
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        
        $this->logger->info('ArticleVoter called', [
            'attribute' => $attribute,
            'user' => $user?->getUserIdentifier(),
            'article_id' => $subject->getId(),
        ]);
        
        // Votre logique...
    }

⚠️ Attention en production :

  • Désactiver les logs debug sensibles
  • Ne pas logger d'informations personnelles
  • Surveiller les performances

🚀 Cas d'usage avancés

1. ACL avec relations complexes

Voter pour un système de projet d'équipe
class ProjectVoter extends Voter
{
    public const VIEW = 'PROJECT_VIEW';
    public const EDIT = 'PROJECT_EDIT';

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        /** @var Project $project */
        $project = $subject;
        $user = $token->getUser();

        return match($attribute) {
            self::VIEW => $this->canView($project, $user),
            self::EDIT => $this->canEdit($project, $user),
            default => false,
        };
    }

    private function canView(Project $project, User $user): bool
    {
        // Le propriétaire peut voir
        if ($project->getOwner() === $user) {
            return true;
        }
        
        // Les membres de l'équipe peuvent voir
        if ($project->getTeamMembers()->contains($user)) {
            return true;
        }
        
        // Les projets publics sont visibles par tous
        return $project->isPublic();
    }

    private function canEdit(Project $project, User $user): bool
    {
        // Seul le propriétaire et les éditeurs peuvent modifier
        return $project->getOwner() === $user 
            || $project->getEditors()->contains($user);
    }
}

2. ACL temporelles

private function canEdit(Article $article, User $user): bool
{
    // Vérification de propriétaire
    if ($article->getAuthor() !== $user) {
        return false;
    }
    
    // Interdire l'édition après 24h
    $deadline = $article->getCreatedAt()->modify('+24 hours');
    if (new \DateTimeImmutable() > $deadline) {
        return false;
    }
    
    return true;
}

3. ACL avec quotas

private function canCreate(User $user): bool
{
    // Vérifier le quota d'articles par mois
    $thisMonth = new \DateTimeImmutable('first day of this month');
    $articleCount = $this->articleRepository->countByAuthorSince($user, $thisMonth);
    
    $maxArticlesPerMonth = match(true) {
        $this->hasRole($user, 'ROLE_PREMIUM') => 50,
        $this->hasRole($user, 'ROLE_USER') => 10,
        default => 0,
    };
    
    return $articleCount < $maxArticlesPerMonth;
}

🎯 Conclusion et ressources

🎓 Ce que vous avez appris

✅ Compétences acquises :

  • Concepts fondamentaux : Authentification vs Autorisation
  • Configuration Symfony : Firewalls, providers, encoders
  • Access Control : Règles globales dans security.yaml
  • Voters personnalisés : Logique métier fine
  • Intégration : Contrôleurs, templates, tests
  • Outils avancés : Expressions, hiérarchie, debugging

📚 Ressources pour approfondir

📖 Documentation officielle :

🎬 Tutoriels vidéo :

  • SymfonyCasts - Security Course
  • Grafikart - Sécurité Symfony
  • YouTube - Symfony Security tutorials

🚀 Prochaines étapes

🎯 Pour continuer votre apprentissage :

  • Pratiquez : Créez un projet avec différents rôles
  • Testez : Écrivez des tests unitaires pour vos Voters
  • Explorez : API Platform, JWT, OAuth
  • Contribuez : Participez à des projets open source

👨‍💻 Questions ?

N'hésitez pas à poser vos questions !

La sécurité est un domaine vast et en constante évolution.

🎉 Bonne route dans vos projets Symfony ! 🎉