How to Mix Security and Form with Symfony
In some applications, it could be required to disable some form fields depending on user’s roles.
In this article, we will see how to implement this feature thanks to a simple example: a blog engine.
Section intitulée introduction-amp-application-s-bootstrapIntroduction & application’s bootstrap
To save time, we will use Symfony 4.1, Symfony Flex and the MakerBundle. All the code is published on Github. You can clone the repository to follow each steps.
Thanks to a few commands and a few files edit, we build a working blog in few minutes:
composer create-project symfony/website-skeleton form-and-security
cd form-and-security
composer req maker
bin/console make:entity Article
# We add `title`, and `content`
bin/console make:entity Admin
# We add `name`
bin/console make:entity Article
# We add the relationship `author` between Article and 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
Now we can setup the Security Component. To be simple, all users will have the
same password: password
. They will login with a form.
To keep this article light, we will not detail this feature here. But you can browse the diff on Github
Section intitulée implementationImplementation
Only admins with role ROLE_ADMIN
are allowed to update the title
of the article.
Admins with role ROLE_EDITOR
are allowed to update the content
of the article.
In order to get a nice UX, we choose to keep the field in the form, but to disable it. Visually, this one will be grayed but visible.
For the DX, we want to configure the visibility with an option on the type:
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'is_granted_attribute' => 'ROLE_ADMIN',
])
// ...
;
}
}
To do that, we need to create a form extension.
As the option is_granted_attribute
will be available on all Type
s, the
extension should extend FormType
. Indeed, each Type
s extends (at some
point) 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;
}
}
We can now develop the most interesting part: disabling a widget according to user’s roles.
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);
}
}
}
If the logged user doesn’t have the role contained in the is_granted_attribute
option, then the widget will be disabled. This means that the widget will get
the disable="disabled"
HTML attribute, and in a recursive way.
However, we just open a security breach. IF a malicious user delete the HTML
attribute, they will be able to update the value. To mitigate this breach, we
should not use the data provided by the user. A Listener
will do the job:
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à! Quite easy to do, isn’t it?
Section intitulée alternative-way-to-doAlternative way to do
There is an alternative way to do that. It relies on an not-so-known option of
the OptionResolver
.
class SecurityExtension extends AbstractTypeExtension
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'is_granted_attribute' => null,
'disabled' => function (Options $options) {
return $this->isGranted($options);
},
]);
}
Here, disabled
option is lazily computed and can depends on others options.
So there are no need for finishView
, disableView
, and buildForm
methods.
The code is really simpler, smaller! You may wonder: « But Greg, why did you choose complexity? ». Actually, this code is less flexible and will forbid us to implement the next chapter. 😜
Section intitulée remove-the-widget-do-not-disable-itRemove the widget, do not disable it
In some cases, like sensitive data, it can be better to remove the widget
instead of disabling it. We will add a new option: is_granted_hide
to do that.
Thus, we will use it in the buildForm
method.
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 what-about-new-articlesWhat about new articles?
The title is mandatory. As Alice is not allowed to update the title of an article, she can not create new articles. Damn It! In some situations, we want to disable theses protections.
To fix this issue, we will add a new option: is_granted_disabled
. If its value
is true
, there is nothing to do.
You can find the diff of this feature on Github.
Section intitulée conclusionConclusion
Thanks to FormExtension
s (among other things), The Form Component is really
powerful and extensible. It is very important to know this extension point as
soon as you want to achieve forms with complex business rules.
You can browse the code of this chapter on Github.
In the next episode, we will see how to use the current article to grant access or not.
Commentaires et discussions
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
Ouibus a pour ambition de devenir la référence du transport en bus longue distance. Dans cette optique, les enjeux à venir de la compagnie sont nombreux (vente multi-produit, agrandissement du réseau, diminution du time-to-market, amélioration de l’expérience et de la satisfaction client) et ont des conséquences sur la structuration de la nouvelle…
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…
À l’occasion de la 12e édition du concours Europan Europe, JoliCode a conçu la plateforme technique du concours. Ce site permet la présentation des différents sites pour lesquels il y a un appel à projets, et encadre le processus de recueil des projets soumis par des milliers d’architectes candidats. L’application gère également toute la partie post-concours…