Comment sécuriser des champs de formulaire avec Symfony
Dans certaines applications, il peut être nécessaire de désactiver certains champs d’un formulaire en fonction des rôles de l’utilisateur connecté.
Dans cet article, nous allons voir comment réaliser cette fonctionnalité à travers un exemple simple : un moteur de blog.
Section intitulée introduction-et-bootstrap-de-l-applicationIntroduction et bootstrap de l’application
Nous allons utiliser Symfony 4.1, Symfony Flex et le MakerBundle pour gagner du temps. Tout le code est publié sur Github. N’hésitez pas à cloner le dépôt pour jouer avec l’application.
Grâce à quelques commandes et quelques modifications de fichiers à la main, nous avons un blog à peu près fonctionnel en très peu de temps :
composer create-project symfony/website-skeleton form-and-security
cd form-and-security
composer req maker
bin/console make:entity Article
# Nous ajoutons juste un `title`, et un `content`
bin/console make:entity Admin
# Nous ajoutons juste un `name`
bin/console make:entity Article
# Nous ajoutons la relation `author` entre Article et Admin
bin/console doctrine:database:create
bin/console make:migration
bin/console doc:migration:migrate
bin/console make:crud Admin
bin/console make:crud Article
Maintenant, nous allons pouvoir mettre en place le composant Security de Symfony. Dans
un soucis de simplicité, tous les utilisateurs auront le même mot de passe :
password
et ils pourront se connecter via un formulaire.
Pour ne pas alourdir cet article inutilement, nous n’allons pas détailler tout ce processus. Vous pouvez néanmoins retrouver le diff sur Github
Section intitulée mise-en-place-des-regles-de-gestionMise en place des règles de gestion
Pour l’administration d’un article, nous voulons que seulement les
administrateurs avec le rôle ROLE_ADMIN
puissent changer le titre d’un article.
Les administrateurs ayant le rôle ROLE_EDITOR
pourront, quant à eux, seulement
changer le contenu de l’article.
Pour avoir une expérience utilisateur agréable, nous décidons de laisser visible mais de désactiver le champ que l’utilisateur ne peut pas modifier. Visuellement, celui-ci sera donc grisé mais lisible.
Du côté expérience développeur, nous voulons pouvoir configurer la visibilité d’un champ grâce à une option sur le type :
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'is_granted_attribute' => 'ROLE_ADMIN',
])
// ...
;
}
}
Pour configurer des champs de formulaire en fonction du rôle d’un utilisateur, il faut créer une extension de formulaire.
Comme l’option is_granted_attribute
sera disponible sur tous les Type
s
de l’application, l’extension devra étendre FormType
. En effet, chaque *Type
fini par étendre FormType
:
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class SecurityExtension extends AbstractTypeExtension
{
private $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_granted_attribute' => null,
]);
}
public function getExtendedType()
{
return FormType::class;
}
}
Nous allons pouvoir maintenant développer la partie la plus intéressante : désactiver un widget en fonction des rôles de l’utilisateur :
class SecurityExtension extends AbstractTypeExtension
{
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($this->isGranted($options)) {
return;
}
$this->disableView($view);
}
private function isGranted(FormInterface $form, array $options)
{
if (!$options['is_granted_attribute']) {
return true;
}
if ($this->authorizationChecker->isGranted($options['is_granted_attribute'])) {
return true;
}
return false;
}
private function disableView(FormView $view)
{
$view->vars['attr']['disabled'] = true;
foreach ($view as $child) {
$this->disableView($child);
}
}
}
Si l’utilisateur connecté n’a pas le rôle qui est contenu dans l’option
is_granted_attribute
, alors le widget sera désactivé. C’est-à-dire qu’il aura
l’attribut HTML disable="disabled"
et ce de manière récursive.
Cependant, nous venons de créer une faille de sécurité dans l’application. Si un
utilisateur malicieux supprime l’attribut HTML, il sera en mesure de changer la
valeur du widget. Pour mitiger cette faille, il ne faut pas utiliser les données
que l’utilisateur a envoyé. Un Listener
dans l’extension fera l’affaire :
class SecurityExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['is_granted_attribute']) {
return;
}
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
if ($this->isGranted($options)) {
return;
}
$event->setData($event->getForm()->getViewData());
});
}
}
Et Voilà ! Ce n’était pas très compliqué ?
Section intitulée une-facon-alternative-de-faireUne façon alternative de faire
Il aurait été possible d’avoir un code plus minimaliste en utilisant une
fonctionnalité peu connue de l’OptionResolver
:
class SecurityExtension extends AbstractTypeExtension
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_granted_attribute' => null,
'disabled' => function (Options $options) {
return $this->isGranted($options);
},
]);
}
}
Ici, l’option disabled
est évaluée de manière lazy
en fonction des autres
options. Il n’y a alors plus besoin des méthodes finishView
, disableView
,
buildForm
.
Cependant, cette façon de faire n’est pas aussi flexible que celle que nous avons vu dans cet article. Et elle nous aurait bloqué pour le chapitre suivant 😜.
Section intitulée supprimer-un-champ-au-lieu-de-le-desactiverSupprimer un champ au lieu de le désactiver
Dans certain cas, comme par exemple pour des données sensibles, il est
préférable de ne pas afficher le champ. Pour ce faire, il faut ajouter
une nouvelle option : is_granted_hide
et l’utiliser dans la méthode
buildForm
:
class SecurityExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['is_granted_attribute']) {
return;
}
if ($options['is_granted_hide']) {
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
if ($this->isGranted($options)) {
return;
}
$form = $event->getForm();
$form->getParent()->remove($form->getName());
});
} else {
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
if ($this->isGranted($options)) {
return;
}
$event->setData($event->getForm()->getViewData());
});
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_granted_attribute' => null,
'is_granted_hide' => false,
]);
}
}
Section intitulée que-faire-pour-les-nouveaux-articlesQue faire pour les nouveaux articles ?
Le titre d’un article est obligatoire. Comme Alice n’a pas le droit de modifier le titre d’un article elle ne peut pas créer d’article. Sacrebleu ! Il existe certain cas légitime où l’on veut désactiver ces protections.
Pour résoudre ce problème, il suffit d’ajouter une nouvelle option
is_granted_disabled
. Si la valeur de cette option est true
, alors il suffit
de ne rien faire.
Vous pouvez retrouver le diff de ce changement sur Github.
Section intitulée conclusionConclusion
Le composant de formulaire, grâce notamment aux FormExtension
s, permet de
changer facilement le comportement d’un formulaire. Il est primordial de bien
connaître ce point d’extension de Symfony dès que l’on veut réaliser des
formulaires avec des règles métiers complexes.
Vous pouvez aussi retrouver tout le code de ce premier chapitre sur Github.
Dans le prochain épisode, nous verrons comment contrôler la sécurité d’un formulaire en fonction des rôles de l’utilisateur, mais aussi de l’article en cours d’édition.
Commentaires et discussions
Comprendre et éviter les attaques CSRF grâce à Symfony
CSRF veut dire Cross-Site Request Forgery en anglais, une traduction française pourrait être « Falsification de requêtes inter-sites ». Dans cet article, nous allons faire un rappel de ce qu’est une attaque CSRF et comment Symfony nous en protège. Puis au travers d’un exemple…
Lire la suite de l’article Comprendre et éviter les attaques CSRF grâce à Symfony
Nos articles sur le même sujet
Nos formations sur ce sujet
Notre expertise est aussi disponible sous forme de formations professionnelles !
Symfony avancée
Découvrez les fonctionnalités et concepts avancés de Symfony
Ces clients ont profité de notre expertise
Au fil de notre collaboration avec Deezer, nous avons été impliqués dans plusieurs initiatives visant à optimiser les performances de leur plateforme. Notre engagement initial s’est concentré sur le soutien et le conseil à l’équipe « Product Features » lors de leur projet de migration en cours. Nous avons apporté notre expertise pour résoudre…
L’équipe de Finarta a fait appel à JoliCode pour le développement de leur plateforme Web. Basée sur le framework Symfony 2, l’application est un réseau privé de galerie et se veut être une place de communication et de vente d’oeuvres d’art entre ses membres. Pour cela, de nombreuses règles de droits ont été mises en places et une administration poussée…
JoliCode a été sollicité pour accompagner le développement de la nouvelle version du site. Conçue avec le framework Symfony2, cette nouvelle version bénéficie de la performance et la fiabilité du framework français. Reposant sur des technologies comme Elasticsearch, cette nouvelle version tend à offrir une expérience optimale à l’internaute. Le développement…