Buy

Ok, new challenge! I only want this edit button to be visible and accessible if the user has ROLE_SUPERADMIN. This turns out to be a bit complicated... in part because there are two sides to it.

First, we need truly block access to that action... so that a clever user can't just hack the URL and start editing! And second, we need to actually hide the link... so that our less-than-super-admin users don't get confused.

Preventing Action Access by Role

First, let's lock down the actual controller action. How? Now we know two ways: by overriding the editAction() in UserController and adding a security check or by adding a PRE_EDIT event listener. Let's use events!

Subscribe to a second event: EasyAdminEvents::PRE_EDIT set to onPreEdit:

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 5
use JavierEguiluz\Bundle\EasyAdminBundle\Event\EasyAdminEvents;
... lines 7 - 12
class EasyAdminSubscriber implements EventSubscriberInterface
{
... lines 15 - 23
public static function getSubscribedEvents()
{
return [
EasyAdminEvents::PRE_EDIT => 'onPreEdit',
... line 28
];
}
... lines 31 - 59
}

Once again, I'll hit Alt+Enter as a shortcut to create that method for me:

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 7
use Symfony\Component\EventDispatcher\GenericEvent;
... lines 9 - 12
class EasyAdminSubscriber implements EventSubscriberInterface
{
... lines 15 - 31
public function onPreEdit(GenericEvent $event)
{
... lines 34 - 37
}
... lines 39 - 59
}

And just like before... we don't really know what the $event looks like. So, dump it!

Now, as soon as I hit edit... we see the dump! Check this out: this time, the subject property is actually an array. But, it has a class key set to the User class. We can use that to make sure we only run our code when we're editing a user.

In other words, $config = $event->getSubject() and if $config['class'] is equal to our User class, then we want to check security:

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 12
class EasyAdminSubscriber implements EventSubscriberInterface
{
... lines 15 - 31
public function onPreEdit(GenericEvent $event)
{
$config = $event->getSubject();
if ($config['class'] == User::class) {
... line 36
}
}
... lines 39 - 59
}

Let's call a new method... that we'll create in a moment: $this->denyAccessUnlessSuperAdmin():

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 12
class EasyAdminSubscriber implements EventSubscriberInterface
{
... lines 15 - 31
public function onPreEdit(GenericEvent $event)
{
$config = $event->getSubject();
if ($config['class'] == User::class) {
$this->denyAccessUnlessSuperAdmin();
}
}
... lines 39 - 59
}

At the bottom, add that: private function denyAccessUnlessSuperAdmin():

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 12
class EasyAdminSubscriber implements EventSubscriberInterface
{
... lines 15 - 53
private function denyAccessUnlessSuperAdmin()
{
... lines 56 - 58
}
}

Now... we just need to check to see if the current user has ROLE_SUPERADMIN. How? Via the "authorization checker" service. To get it, type-hint a new argument with AuthorizationCheckerInterface. Hit Alt+Enter to create and set that property:

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 9
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
... lines 11 - 12
class EasyAdminSubscriber implements EventSubscriberInterface
{
... line 15
private $authorizationChecker;
public function __construct(TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $authorizationChecker)
{
... line 20
$this->authorizationChecker = $authorizationChecker;
}
... lines 23 - 59
}

Then, back down below, if (!$this->authorizationChecker->isGranted('ROLE_SUPERADMIN'), then throw a new AccessDeniedException():

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 10
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class EasyAdminSubscriber implements EventSubscriberInterface
{
... lines 15 - 53
private function denyAccessUnlessSuperAdmin()
{
if (!$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')) {
throw new AccessDeniedException();
}
}
}

Make sure you use the class from the Security component:

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 10
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
... lines 12 - 61

Oh, and don't forget the new!

61 lines src/AppBundle/Event/EasyAdminSubscriber.php
... lines 1 - 12
class EasyAdminSubscriber implements EventSubscriberInterface
{
... lines 15 - 53
private function denyAccessUnlessSuperAdmin()
{
if (!$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')) {
throw new AccessDeniedException();
}
}
}

See, normally, in a controller, we call $this->denyAccessUnlessGranted(). When we do that, this is actually the exception that is thrown behind the scenes. In other words, we're really doing the same thing that we normally do in a controller.

And... we're done! The service is set to be autowired, so Symfony will know to pass us the authorization checker automatically. Refresh!

Great news! Access denied! Woohoo! I've never been so happy to get kicked out of something. Our user does not have ROLE_SUPERADMIN - just ROLE_ADMIN and ROLE_USER. To double-check our logic, open app/config/security.yml, and, temporarily, for anyone who has ROLE_ADMIN, also give them ROLE_SUPERADMIN:

security:
    role_hierarchy:
        ROLE_ADMIN: [ROLE_MANAGE_GENUS, ROLE_ALLOWED_TO_SWITCH, ROLE_SUPERADMIN]

Now we should have access. Try it again!

Access granted! Comment-out that ROLE_SUPERADMIN.

Hiding the Edit Button

Time for step 2! On the list page, we need to hide the edit link, unless I have the role. This is trickier: there's no official hook inside of EasyAdminBundle to conditionally hide or show actions. But don't worry! Earlier, we overrode the list template so that we could control exactly what actions are displayed. Our new filter_admin_actions() filter lives in EasyAdminExtension:

8 lines app/Resources/views/easy_admin/list.html.twig
... lines 1 - 2
{% block item_actions %}
{% set _list_item_actions = _list_item_actions|filter_admin_actions(item) %}
... lines 5 - 6
{% endblock %}

And we added logic there to hide the delete action for any published genuses:

28 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 6
class EasyAdminExtension extends \Twig_Extension
{
... lines 9 - 18
public function filterActions(array $itemActions, $item)
{
if ($item instanceof Genus && $item->getIsPublished()) {
unset($itemActions['delete']);
}
return $itemActions;
}
}

In other words, we added our own hook to control which actions are displayed. We rock!

To hide the edit action, we'll need the authorization checker again. No problem! Add public function __construct() with one argument: AuthorizationCheckerInterface. Set that on a new property:

41 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 6
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class EasyAdminExtension extends \Twig_Extension
{
private $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
... lines 17 - 39
}

Then, down below, we'll add some familiar code: if $item instanceof User and !$this->authorizationChecker->isGranted('ROLE_SUPERADMIN'), then unset the edit action:

41 lines src/AppBundle/Twig/EasyAdminExtension.php
... lines 1 - 8
class EasyAdminExtension extends \Twig_Extension
{
... lines 11 - 27
public function filterActions(array $itemActions, $item)
{
... lines 30 - 33
if ($item instanceof User && !$this->authorizationChecker->isGranted('ROLE_SUPERADMIN')) {
unset($itemActions['edit']);
}
... lines 37 - 38
}
}

Phew! It's not the easiest thing ever... EasyAdminBundle... but it does get the job done!

Except for one... minor problem... there is also an edit button on the show page. Oh no! It looks like we need to repeat all of this for the show template!

Controlling the Actions in the show Template

But don't worry! With all our knowledge, this should be quick and painless.

Inside of the bundle, find the show template. And inside it, search for "actions". Here we go: block item_actions. To control the actions, we can do a very similar thing as the list template. In fact, copy the list template, and paste it as show.html.twig. Because it's in the right location, it should automatically override the one from the bundle.

Extend that base show.html.twig template:

15 lines app/Resources/views/easy_admin/show.html.twig
{% extends '@EasyAdmin/default/show.html.twig' %}
... lines 2 - 15

Before, we overrode the _list_item_actions variable and then called the parent() function to render the parent block.

But... that actually won't work here! Bananas! Why not? In this case, the variable we need to override is called _show_actions. And... well... it's set right inside the block. That's different from list.html.twig, where the variable was set above the block. This means that if we override _show_actions and then call the parent block, the parent block will re-override our value! Lame!!!

No worries, it just means that we need to override the entire block, and avoid calling parent. Copy the block and, in show.html.twig, paste:

15 lines app/Resources/views/easy_admin/show.html.twig
... lines 1 - 2
{% block item_actions %}
{% set _show_actions = easyadmin_get_actions_for_show_item(_entity_config.name) %}
... line 5
{% set _request_parameters = { entity: _entity_config.name, referer: app.request.query.get('referer') } %}
{{ include('@EasyAdmin/default/includes/_actions.html.twig', {
actions: _show_actions,
request_parameters: _request_parameters,
translation_domain: _entity_config.translation_domain,
trans_parameters: _trans_parameters,
item_id: _entity_id
}, with_context = false) }}
{% endblock item_actions %}

Next, add our filter: set _show_actions = _show_actions|filter_admin_actions:

15 lines app/Resources/views/easy_admin/show.html.twig
... lines 1 - 2
{% block item_actions %}
{% set _show_actions = easyadmin_get_actions_for_show_item(_entity_config.name) %}
{% set _show_actions = _show_actions|filter_admin_actions(entity) %}
{% set _request_parameters = { entity: _entity_config.name, referer: app.request.query.get('referer') } %}
... lines 7 - 14
{% endblock item_actions %}

Remember, we need to pass the entity object as an argument to filter_admin_actions... and that's another difference between show and list. Since this template is for a page that represents one entity, the variable is not called item, it's called entity.

As crazy as that looks, it should do it! Hold you breath, do a dance, and refresh!

Hey! No edit button! Go back to security.yml and re-add ROLE_SUPERADMIN:

security:
    role_hierarchy:
        ROLE_ADMIN: [ROLE_MANAGE_GENUS, ROLE_ALLOWED_TO_SWITCH, ROLE_SUPERADMIN]

Refresh now. Edit button is back. And we can even use it. One of the least-easy things in EasyAdminBundle is now done!

Leave a comment!

  • 2017-08-01 Victor Bocharsky

    Hey screon ,

    The way is the same as we did with EasyAdminExtension::filterActions() in this video. Actually, you just need to find a proper Twig template, override it and implement whatever crazy logic you need. You can do it just with is_granted() Twig function in the template, or create a custom Twig filter/function, move complex checking logic there and call it from the overridden template (similar to ours filterActions()).

    Or, you know, take a look at JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController::createDeleteForm(). Probably you just can to override this method and hide the button there.

    Cheers!

  • 2017-07-28 screon

    Thanks for the suggestion! But I need to apply some logic on user role basis. In the video we've conditionally hidden the delete action on the list and show views, but it didn't mention anything about the edit view, where the delete button is still visible. So basically: if user doesn't have role_admin, hide the delete button. But only for that specific entity.

  • 2017-07-28 Diego Aguiar

    Hey screon

    Do you need to remove the delete action only if certain logic applies to the given object or is it more general ? Because you could remove the action in the config.yml file. Something like this:


    entities:
    your_entity:
    edit:
    actions: ['-delete']

    I hope it helps you, cheers!

  • 2017-07-28 screon

    Hey Ryan, great work on the videos!
    I was wondering how you can dynamically remove the delete action from the edit view? It's seems to be baked into the form itself...