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
Nous avons travaillé en étroite collaboration avec Cerfrance pour améliorer la qualité de leurs projets PHP et Symfony, tout en renforçant les compétences de leur équipe. Notre intervention a consisté à mettre en place une intégration continue (CI), à coacher l’équipe pour l’ajout de fixtures et de tests, à dockeriser l’application, et à installer…
Nous avons épaulé Adrenaline Hunter, juste avant le lancement public de ses offres, sur des problématiques liées à la performance de leur application développée avec Symfony. Nous avons également mis en place un système de workflow des commandes de séjours afin que toutes les actions avec leurs différents partenaires soient réparties avec fiabilité…
Dans le cadre du renouveau de sa stratégie digitale, Orpi France a fait appel à JoliCode afin de diriger la refonte du site Web orpi.com et l’intégration de nombreux nouveaux services. Pour effectuer cette migration, nous nous sommes appuyés sur une architecture en microservices à l’aide de PHP, Symfony, RabbitMQ, Elasticsearch et Docker.