EasyCorp / EasyAdminBundle

EasyAdmin is a fast, beautiful and modern admin generator for Symfony applications.
MIT License
3.99k stars 1.01k forks source link

Show menus and allow actions based on roles and permissions #807

Closed reypm closed 8 years ago

reypm commented 8 years ago

I'm not sure if this has been discussed before or whether the new functionality supports menus, if so my sincere apologies.

Let's say my admin is complete, has all its menus and sub-menus. Is there any way to restrict access to certain menus|sub-menus and possibly actions based on roles and permissions in EasyAdmin?

Let's said:

Is this complex setup allowed in EasyAdmin? If not any ideas to achieve something like this?

javiereguiluz commented 8 years ago

Thanks for proposing this feature!

For now, this feature is not available. It's in our long-term roadmap, so it will take a while before we implement it. The reason is that it's very hard to do it right.

These are the alternatives that I can give you:

1) If hiding things is to make it easier to use, you can use a CSS trick. Add the user role as a CSS class in the body of the layout:

{% block body_class %}{{ app.user.roles|default|([])join(' ')|lower }}{% endblock %}

Then, in your custom CSS you just hide the parts you don't want those users to see:

body.role_user .sidebar-menu ... { display: none; }

2) If you really want to remove those elements from the page, you'll have to override the default templates. Luckily, after the last big redesign, the code of the templates will be stable and your maintenance work will be small.

If you must do a heavy use of security and roles, another alternative is to forget about EasyAdmin for that backend and use Sonata instead.

I'm closing this issue because we are ware of this feature and we labeled it with future + feature so we don't loose track of it. Thanks!

reypm commented 8 years ago

@javiereguiluz Excellent! glad to help improve this bundle and not I don't go back to Sonata I'd like to much EasyAdmin :smile:

"If you really want to remove those elements from the page, you'll have to override the default templates. Luckily, after the last big redesign, the code of the templates will be stable and your maintenance work will be small." can you put a little example of what is on your mind regarding this one? I can use the first approach for now but would also like to play with 2nd but need a little example first, could you add it?

reypm commented 8 years ago

Hi there @javiereguiluz I am using your snippet:

{#app/Resources/views/easy_admin/layout.html.twig#}    
{% extends '@EasyAdmin/default/layout.html.twig' %}
{% block body_class %}{{ app.user.roles|default|([])join('')|lower }}{% endblock %}

But I am getting this error:

Unexpected token "punctuation" of value "(" ("name" expected) in easy_admin/layout.html.twig at line 3.

Could you give me some advice?

javiereguiluz commented 8 years ago

This line:

{% block body_class %}{{ app.user.roles|default|([])join('')|lower }}{% endblock %}

should be:

{% block body_class %}{{ app.user.roles|default([])|join(' ')|lower }}{% endblock %}

Anyway, keep in mind that this trick is just to hide things in an unsecure way. If you need real security, you can't use this.

reypm commented 8 years ago

@javiereguiluz yes, thx I can use this for now, it's a internal app and I don't know hackers will go after it lol.

After user gets logged in I can see this in the web debug toolbar:

Roles   [ROLE_ADMIN, ROLE_USER]
Inherited Roles [ROLE_USER, ROLE_CHATTER]

But those values are not in the body class, should them be there?

javiereguiluz commented 8 years ago

This code app.user.roles|default([])|join(' ')|lower should make your <body> look like this:

<body ... class="... role_admin role_user" ... >
reypm commented 8 years ago

@javiereguiluz I don't want to go back to Sonata and I love EasyAdmin I'll start working on this feature on my own but I need a few ideas on how to start and where from this means what should I understand, where I need to look and hook or so on the EasyAdmin side. Can you give me this? Perhaps will not be a good solution what I would do but maybe could be integrated sooner if I end it. For now I will use FOSUser roles and kind of permissions for give access to menus and/or actions on controllers.

javiereguiluz commented 8 years ago

I haven't tested this, but you could start as follows:


Quick example for the menu.html.twig template:

BEFORE:

<ul class="sidebar-menu">
    {% block main_menu %}
        {% for item in easyadmin_config('design.menu') %}
            <li class="">
                {{ helper.render_menu_item(item) }}
        ...

AFTER:

<ul class="sidebar-menu">
    {% block main_menu %}
        {% for item in easyadmin_config('design.menu') if item.role in app.user.roles %}
            <li class="">
                {{ helper.render_menu_item(item) }}
        ...
reypm commented 8 years ago

@javiereguiluz my idea here is define a table where I have the menu name (a label) and four basic actions: CRUD, then I can assign as many roles as I want to each of them and them apply using your code snippet. I am not sure if this is what you said at first point of your answer, I am right? If not could you improve your idea so I can catch it? Thanks

Pierstoval commented 8 years ago

@reypm EasyAdmin should not have entities and should not create new tables, so I hope this CRUD table is only for your app.

The implementation we need is to:

  1. Set up the role property for everything in the backend (menus, actions, links, buttons, properties, controller method, etc.)
  2. Everywhere we need the role, we must check that there is a User in the security context and that it has the needed Role. Search for the user in the context is mandatory, because you might not have one if the firewall is configured with anonymous: false for example, or if the anonymous user is linked to no User object.

Don't worry, once you will make the PR we'll help you ;)

reypm commented 8 years ago

@Pierstoval Even if I am not fully understand the first point (I will need a little more here, again due to my poor knowledge) what happen with dynamic roles? You're thinking only on roles defined on the security.yml or I am wrong? In my case I would like to use also roles from FOSUserBundle because the business rules can define a new roles (same as permissions) after the project ends.

javiereguiluz commented 8 years ago

I still think that if you make a heavy use of security/roles ... you should use Sonata or a custom development. Using EasyAdmin may hurt you in this case and make everything more complicated than it should be.

reypm commented 8 years ago

@javiereguiluz the problem with Sonata is the bundle isn't ready yet for use with Symfony 2.8 or 3.0 I know they are working hard to get it done but still not ready and therefore other bundles as UserBundle aren't ready yet so I still sticky to EasyAdmin anyway this is not a thing that I will need soon and the project is a long term project so perhaps in the middle of the year I still working on the same project and you could add this feature as labeled in the future+feature ;)

crossplatformdev commented 7 years ago

@reypm If you want, you can use this twig function:

{# example security usage, see below #}
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    {# ... #}
{% endif %}
{# you can put any role up there #}

It's way better than hide content with only CSS ;-)

In order to do so, I had to override the templates by placing the @javiereguiluz folder from vendors and placing it under src and renamed the folder and subfolder, as it is indicated in Symfony book, but at first it did not work, because I was not able to match the namespace, reflecting it on the directory tree (I ask here help for this topic, as it is very related).

Just to try the twig function out, I renamed the namespace and the backslashed strings containing also the namespace and made Symfony believe that it is one of my sources (I've also modified the bundle initialization in AppKernel.php).

It works indeed, thus, I am not comfortable with having to do such a mess (rename the namespace), in order to do what Symfony is supposed to do for me, just using a valid directory tree. I ask @javiereguiluz support for that.

Also, I've successfully added these snippets on the AdminController.php file, which also works:

[edit, show]:

    $id = $this->request->query->get('id');
    $user = $this->get('security.token_storage')->getToken()->getUser();
    if ($this->isGranted("ROLE_SUPER_ADMIN") || $id === strval($user->getId())) {
         // ... Your role-granted or user id code goes here ...
    } else {
        throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
    }
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        //$this->renderForbiddenActionError($this->request->query->get('action'));
        throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
    }

[list, new, index]:

    $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN', null, "You have not the right permissions to render this page!");

I just have two roles, and this trick does fit my needs. I just only need to work on the webflow (navigation) now, as the usual easy admin won't work for regular users, but it will still work great to users with ROLE_SUPER_ADMIN.

BTW, the function renderForbiddenActtionError(), did not find the template. Not even present on vendor folder, version ^1.14.

Very related to mine: http://symfony.com/doc/current/cookbook/bundles/override.html Very related to @reypm: http://symfony.com/doc/current/book/security.html

crossplatformdev commented 7 years ago

By the way, I finally solved my issue.

To override EasyAdminBundle controller or views, you have to follow the steps described in here:

http://symfony.com/doc/current/cookbook/bundles/override.html

javiereguiluz commented 7 years ago

@crossplatformdev I don't recommend you to do that. EasyAdmin provides its own overriding mechanism which is easier and more powerful than the default bundle overriding mechanism.

crossplatformdev commented 7 years ago

@javiereguiluz I see your point, thank you.

I followed the documentation to override the controller and also the views.

Here is how to override the Controller: https://github.com/javiereguiluz/EasyAdminBundle/blob/master/Resources/doc/book/7-complex-dynamic-backends.md

My AdminController:

//OhmyBundle/Controller/AdminController

/*
 * This file is part of the EasyAdminBundle.
 *
 * (c) Javier Eguiluz <javier.eguiluz@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace OhmyBundle\Controller;

use Symfony\Component\Routing\Annotation\Route;
use JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController as BaseAdminController;
use JavierEguiluz\Bundle\EasyAdminBundle\Event\EasyAdminEvents;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpFoundation\Request as Request;

class AdminController extends BaseAdminController
{
    /**
     * @Route("/", name="easyadmin")
     * @Route("/", name="admin")
     *
     * The 'admin' route is deprecated since version 1.8.0 and it will be removed in 2.0.
     *
     * @param Request $request
     *
     * @return RedirectResponse|Response
     */
    public function indexAction(Request $request) {
        $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN', null, "You have not the right permissions to render this page!");
        $this->initialize($request);

        if (null === $request->query->get('entity')) {
            return $this->redirectToBackendHomepage();
        }

        $action = $request->query->get('action', 'list');
        if (!$this->isActionAllowed($action)) {
            throw new ForbiddenActionException(array('action' => $action, 'entity' => $this->entity['name']));
        }

        return $this->executeDynamicMethod($action . '<EntityName>Action');
    }

    /**
     * The method that is executed when the user performs a 'list' action on an entity.
     *
     * @return Response
     */
    protected function listAction() {
        $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN', null, "You have not the right permissions to render this page!");
        $this->dispatch(EasyAdminEvents::PRE_LIST);

        $fields = $this->entity['list']['fields'];
        $paginator = $this->findAll($this->entity['class'], $this->request->query->get('page', 1), $this->config['list']['max_results'], $this->request->query->get('sortField'), $this->request->query->get('sortDirection'));

        $this->dispatch(EasyAdminEvents::POST_LIST, array('paginator' => $paginator));

        return $this->render($this->entity['templates']['list'], array(
                    'paginator' => $paginator,
                    'fields' => $fields,
                    'delete_form_template' => $this->createDeleteForm($this->entity['name'], '__id__')->createView(),
        ));
    }

    /**
     * The method that is executed when the user performs a 'edit' action on an entity.
     *
     * @return RedirectResponse|Response
     */
    protected function editAction() {
        $this->dispatch(EasyAdminEvents::PRE_EDIT);

        $id = $this->request->query->get('id');
        $user = $this->get('security.token_storage')->getToken()->getUser();
        if ($this->isGranted("ROLE_SUPER_ADMIN") || $id === strval($user->getId())) {

        } else {
            throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
            //$this->renderForbiddenActionError($this->request->query->get('action'));
        }
        if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
            //$this->renderForbiddenActionError($this->request->query->get('action'));
            throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
        }

        $easyadmin = $this->request->attributes->get('easyadmin');
        $entity = $easyadmin['item'];

        if ($this->request->isXmlHttpRequest() && $property = $this->request->query->get('property')) {
            $newValue = 'true' === strtolower($this->request->query->get('newValue'));
            $fieldsMetadata = $this->entity['list']['fields'];

            if (!isset($fieldsMetadata[$property]) || 'toggle' !== $fieldsMetadata[$property]['dataType']) {
                throw new \RuntimeException(sprintf('The type of the "%s" property is not "toggle".', $property));
            }

            $this->updateEntityProperty($entity, $property, $newValue);

            return new Response((string) $newValue);
        }

        $fields = $this->entity['edit']['fields'];

        $editForm = $this->executeDynamicMethod('create<EntityName>EditForm', array($entity, $fields));
        $deleteForm = $this->createDeleteForm($this->entity['name'], $id);

        $editForm->handleRequest($this->request);
        if ($editForm->isValid()) {
            $this->dispatch(EasyAdminEvents::PRE_UPDATE, array('entity' => $entity));

            $this->executeDynamicMethod('preUpdate<EntityName>Entity', array($entity));
            $this->em->flush();

            $this->dispatch(EasyAdminEvents::POST_UPDATE, array('entity' => $entity));

            $refererUrl = $this->request->query->get('referer', '');

            return !empty($refererUrl) ? $this->redirect(urldecode($refererUrl)) : $this->redirect($this->generateUrl('easyadmin', array('action' => 'list', 'entity' => $this->entity['name'])));
        }

        $this->dispatch(EasyAdminEvents::POST_EDIT);

        return $this->render($this->entity['templates']['edit'], array(
                    'form' => $editForm->createView(),
                    'entity_fields' => $fields,
                    'entity' => $entity,
                    'delete_form' => $deleteForm->createView(),
        ));
    }

    /**
     * The method that is executed when the user performs a 'show' action on an entity.
     *
     * @return Response
     */
    protected function showAction() {
        $this->dispatch(EasyAdminEvents::PRE_SHOW);

        $id = $this->request->query->get('id');
        $user = $this->get('security.token_storage')->getToken()->getUser();
        if ($this->isGranted("ROLE_SUPER_ADMIN") || $id === strval($user->getId())) {

        } else {
            //$this->renderForbiddenActionError($this->request->query->get('action'));
            throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
        }
        if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
            //$this->renderForbiddenActionError($this->request->query->get('action'));
            throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
        }

        $easyadmin = $this->request->attributes->get('easyadmin');
        $entity = $easyadmin['item'];

        $fields = $this->entity['show']['fields'];
        $deleteForm = $this->createDeleteForm($this->entity['name'], $id);

        $this->dispatch(EasyAdminEvents::POST_SHOW, array(
            'deleteForm' => $deleteForm,
            'fields' => $fields,
            'entity' => $entity,
        ));

        return $this->render($this->entity['templates']['show'], array(
                    'entity' => $entity,
                    'fields' => $fields,
                    'delete_form' => $deleteForm->createView(),
        ));
    }

    /**
     * The method that is executed when the user performs a 'new' action on an entity.
     *
     * @return RedirectResponse|Response
     */
    protected function newAction() {
        $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN', null, "You have not the right permissions to render this page!");
        $this->dispatch(EasyAdminEvents::PRE_NEW);

        $entity = $this->executeDynamicMethod('createNew<EntityName>Entity');

        $easyadmin = $this->request->attributes->get('easyadmin');
        $easyadmin['item'] = $entity;
        $this->request->attributes->set('easyadmin', $easyadmin);

        $fields = $this->entity['new']['fields'];

        $newForm = $this->executeDynamicMethod('create<EntityName>NewForm', array($entity, $fields));

        $newForm->handleRequest($this->request);
        if ($newForm->isValid()) {
            $this->dispatch(EasyAdminEvents::PRE_PERSIST, array('entity' => $entity));

            $this->executeDynamicMethod('prePersist<EntityName>Entity', array($entity));

            $this->em->persist($entity);
            $this->em->flush();

            $this->dispatch(EasyAdminEvents::POST_PERSIST, array('entity' => $entity));

            $refererUrl = $this->request->query->get('referer', '');

            return !empty($refererUrl) ? $this->redirect(urldecode($refererUrl)) : $this->redirect($this->generateUrl('easyadmin', array('action' => 'list', 'entity' => $this->entity['name'])));
        }

        $this->dispatch(EasyAdminEvents::POST_NEW, array(
            'entity_fields' => $fields,
            'form' => $newForm,
            'entity' => $entity,
        ));

        return $this->render($this->entity['templates']['new'], array(
                    'form' => $newForm->createView(),
                    'entity_fields' => $fields,
                    'entity' => $entity,
        ));
    }

Here is how to override templates: https://github.com/javiereguiluz/EasyAdminBundle/issues/464 (assume they are under 'src\OhmyBundle\Resources\views\EasyAdminBundle\views\'). My config.yml is as follows:

easy_admin:
     entities:
         OhmyUser:
             templates:
                 layout: '@Ohmy/EasyAdminBundle/views/layout.html.twig'
                 edit: '@Ohmy/EasyAdminBundle/views/edit.html.twig'
                 list: '@Ohmy/EasyAdminBundle/views/list.html.twig'
                 new: '@Ohmy/EasyAdminBundle/views/new.html.twig'
                 show: '@Ohmy/EasyAdminBundle/views/show.html.twig'
                 form: '@Ohmy/EasyAdminBundle/views/form.html.twig'
                 flash_messages: '@Ohmy/EasyAdminBundle/views/flash_messages.html.twig'
                 paginator: '@Ohmy/EasyAdminBundle/views/paginator.html.twig'
                 field_array: '@Ohmy/EasyAdminBundle/views/field_array.html.twig'
                 field_association: '@Ohmy/EasyAdminBundle/views/field_association.html.twig'
                 field_bigint: '@Ohmy/EasyAdminBundle/views/field_bigint.html.twig'
                 field_boolean: '@Ohmy/EasyAdminBundle/views/field_boolean.html.twig'
                 field_date: '@Ohmy/EasyAdminBundle/views/field_date.html.twig'
                 field_datetime: '@Ohmy/EasyAdminBundle/views/field_datetime.html.twig'
                 field_datetimetz: '@Ohmy/EasyAdminBundle/views/field_datetimetz.html.twig'
                 field_decimal: '@Ohmy/EasyAdminBundle/views/field_decimal.html.twig'
                 field_float: '@Ohmy/EasyAdminBundle/views/field_float.html.twig'
                 field_id: '@Ohmy/EasyAdminBundle/views/field_id.html.twig'
                 field_image: ':easy_admin:field_image.html.twig'
                 field_integer: '@Ohmy/EasyAdminBundle/views/field_integer.html.twig'
                 field_simple_array: '@Ohmy/EasyAdminBundle/views/field_simple_array.html.twig'
                 field_smallint: '@Ohmy/EasyAdminBundle/views/field_smallint.html.twig'
                 field_string: '@Ohmy/EasyAdminBundle/views/field_string.html.twig'
                 field_text: '@Ohmy/EasyAdminBundle/views/field_text.html.twig'
                 field_time: '@Ohmy/EasyAdminBundle/views/field_time.html.twig'
                 field_toggle: '@Ohmy/EasyAdminBundle/views/field_toggle.html.twig'
                 label_empty: '@Ohmy/EasyAdminBundle/views/label_empty.html.twig'
                 label_inaccessible: '@Ohmy/EasyAdminBundle/views/label_inaccessible.html.twig'
                 label_null: ':easy_admin:label_null.html.twig'
                 label_undefined: '@Ohmy/EasyAdminBundle/views/label_undefined.html.twig'

It would be nice to have a way to override templates globally, apart than per entity.

javiereguiluz commented 7 years ago

It would be nice to have a way to override templates globally, apart than per entity.

There are four ways to override templates: global or per entity and config-based or convention-based. It's explained here: https://github.com/javiereguiluz/EasyAdminBundle/blob/master/Resources/doc/book/3-list-search-show-configuration.md#advanced-design-configuration and here: https://github.com/javiereguiluz/EasyAdminBundle/blob/master/Resources/doc/book/4-edit-new-configuration.md#advanced-design-configuration

Pierstoval commented 7 years ago

Plus, @crossplatformdev , you do not need to throw
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN', null, "You have not the right permissions to render this page!"); on each action, because indexAction is the only route EasyAdmin uses, so you should throw this exception only in indexAction

crossplatformdev commented 7 years ago

@Pierstoval is right, I missed that. I moved the logic to the indexAction, here it is:

    public function indexAction(Request $request) {
        $this->initialize($request);

        if (null === $request->query->get('entity')) {
            return $this->redirectToBackendHomepage();
        }

        $action = $request->query->get('action', 'list');
        if (!$this->isActionAllowed($action)) {
            throw new ForbiddenActionException(array('action' => $action, 'entity' => $this->entity['name']));
        }

        $user = $this->get('security.token_storage')->getToken()->getUser();
        if (!$this->isGranted("ROLE_SUPER_ADMIN")) {
            if ($request->query->get('entity') === 'OhmyUser') {
                $id = $request->query->get('id');

                if (is_null($id)) {
                    //$this->renderForbiddenActionError($this->request->query->get('action'));
                    return $this->redirect($this->generateUrl('easyadmin', array('action' => 'show', 'entity' => $this->entity['name'], 'id' => $user->getId())));
                }

                if (!$this->isGranted("ROLE_SUPER_ADMIN") && is_null($user)) {
                    //$this->renderForbiddenActionError($this->request->query->get('action'));
                    throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
                }

                if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
                    //$this->renderForbiddenActionError($this->request->query->get('action'));
                    throw new \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException();
                }

                $action = $request->query->get('action');
                switch ($action) {
                    default:
                    case 'show':
                        return $this->executeDynamicMethod('show' . '<EntityName>Action');
                    case 'edit':
                        return $this->executeDynamicMethod('edit' . '<EntityName>Action');
                }
            }
        }
        return $this->executeDynamicMethod($action . '<EntityName>Action');
    }

This way I force non ROLE_SUPER_ADMIN users to be only able to view 'edit' and 'show' views. In order to acomplish that, I also had to modify the return sentences of some other actions:

Edit and Delete actions, so it does not go back tu list view unless user is privileged:

return !empty($refererUrl) ? $this->redirect(urldecode($refererUrl)) :
    $this->isGranted('ROLE_SUPER_ADMIN') ? 
        $this->redirect($this->generateUrl('easyadmin', array('action' => 'list', 'entity' => $this->entity['name']))) :
        $this->redirect($this->generateUrl('easyadmin', array('action' => 'show', 'entity' => $this->entity['name'], 'id' => $id)));

On the YAML config, I placed all the view overrides in global scope:

easyadmin:
    design:
        templates:
           layout: '@Ohmy/EasyAdminBundle/views/layout.html.twig'
           edit: '@Ohmy/EasyAdminBundle/views/edit.html.twig'
           list: '@Ohmy/EasyAdminBundle/views/list.html.twig'
           new: '@Ohmy/EasyAdminBundle/views/new.html.twig'
           show: '@Ohmy/EasyAdminBundle/views/show.html.twig'
           # Absolutely everyone. 'form' launches a dependency injection exception, 
           # as the bundle thinks it is another setting

    entities:
        Servers:
            class: OhmyBundle\Entity\Server
            label: 'Servers'
        OhmyUser:
            class: OhmyBundle\Entity\OhmyUser
            label: 'Users'
            list:
                title: "Users list."
                fields: [ {label: "User", property: "usernameCanonical" }, "phone","address","company","vat","firstName","lastName", "password"]
            show:
                title: "Show User."
                fields: [ {label: "User", property: "usernameCanonical" }, "phone","address","company","vat","firstName","lastName", "password"]
                actions: ['edit', '-delete', '-list']
            edit:
                title: "Edit User."
                fields: [ {label: "User", property: "usernameCanonical" }, "phone","address","company","vat","firstName","lastName", "password"]
                actions: ['-delete', '-list']
            new:
                title: "New User."
                fields: [ {label: "User", property: "usernameCanonical" }, "phone","address","company","vat","firstName","lastName", "password"]

So the next step is customizing the twig templates by using is_granted('ROLE_SUPER_ADMIN') function to change views behaviour based on user role. For example, in menu.html.twig, I wrote:

line 20:

        {% if is_granted('ROLE_SUPER_ADMIN') %}
        <a href="{{ path }}" {% if item.target|default(false) %}target="{{ item.target }}"{% endif %}>
            {% if item.icon is not empty %}<i class="fa {{ item.icon }}"></i>{% endif %}
            <span>{{ item.label|trans }}</span>
            {% if item.children|default([]) is not empty %}<i class="fa fa-angle-left pull-right"></i>{% endif %}
        </a>
        {% else %}
            {% if item.label == 'Servers' %}
                <a href="{{ path }}" {% if item.target|default(false) %}target="{{ item.target }}"{% endif %}>
                {% if item.icon is not empty %}<i class="fa {{ item.icon }}"></i>{% endif %}
                <span>{{ item.label|trans }}</span>
                {% if item.children|default([]) is not empty %}<i class="fa fa-angle-left pull-right"></i>{% endif %}
            {% endif %}
        {% endif `%}

Is a simple proof of concept, but it works, and is way better than hiding content by using CSS (as anyone with a F12 key could see the content ;) )

Now I almost have all the functionalities of EasyAdmin for the ROLE_SUPER_ADMIN and a very limited instance of it for regular users.

Now I am wondering if there is a way to customize view fields shown based on roles... On this case, it would be desirable for the admin to view all de FOSUser fields (OhmyUser extends the model class), while keeping regular users to view (and furthermore, edit...) only certain ones.

@javiereguiluz BTW, this function does not work at all:

$this->renderForbiddenActionError($this->request->query->get('action'));

as the twig template that it tries to render does not exist on version "^1.14".

Glideh commented 7 years ago

Here is my small workaround

class EasyAdminController extends BaseEasyAdminController
{
    private function checkPermissions()
    {
        $easyAdmin = $this->request->attributes->get('easyadmin');
        if (isset($easyAdmin['entity']['require_permission'])) {
            $requiredPermission = $easyAdmin['entity']['require_permission'];
        } else {
            $view = $easyAdmin['view'];
            $entity = $easyAdmin['entity']['name'];
            $requiredPermission = 'ROLE_'.strtoupper($view).'_'.strtoupper($entity);
            # Or any other default strategy
        }
        $this->denyAccessUnlessGranted(
            $requiredPermission, null, $requiredPermission.' permission required'
        );
    }

    /**
     * @Route("/", name="easyadmin")
     * @param Request $request
     * @return RedirectResponse|Response
     */
    public function indexAction(Request $request)
    {
        $response = parent::indexAction($request);
        $this->checkPermissions();
        return $response;
    }
#...

The menu part is quite simple.

Pierstoval commented 7 years ago

@Glideh I like your solution! @javiereguiluz , could it be documented/implemented?

bernardpeh commented 7 years ago

hi all, chipping in my 5 cents here. first of all, thanks javier for the wonderful bundle. I think that roles and permission support is still something highly sought after but I agree that doing it correctly requires a lot of thinking and code refactoring. Ideally, we should be able to manipulate the yaml to support role, ie something along the lines of

easy_admin:
    entities:
        UserLog:
            class: AppBundle\Entity\UserLog
            label: admin.link.user_log
            role: ROLE_ADMIN
                 show:
                       actions: ['list', '-edit', '-delete']
                       .... 
            role: ROLE_USER
                list:
                    actions: ['show', '-edit', '-delete']
                    ....

We should be able to implement this using a combination of compilerpass and event listeners. For those who are interested, I've wrote a simple article describing the process. It doesn't cover everything but should points to a direction - https://leanpub.com/practicalsymfony3/read#leanpub-auto-adding-simple-access-control-to-easyadminbundle

Pierstoval commented 7 years ago

Actually, entity metadata is really flexible and you can add custom fields as you want :)

A compiler pass cannot be used because the configuration is processed on cache warmup because it needs doctrine metadata. By the way, this yml syntax does not work, we should find something else like

easy_admin:
    entities:
        UserLog:
            class: AppBundle\Entity\UserLog
            label: admin.link.user_log
            show:
                role: ROLE_ADMIN
                actions: ['list', '-edit', '-delete']
                .... 
            list:
                role: ROLE_USER
                actions: ['show', '-edit', '-delete']
                ....
AntoscencoVladimir commented 7 years ago

@bernardpeh Thanks man, you saved my day

Yondz commented 7 years ago

Hey guys, following to this interesting topic, I'm using something based on @Glideh solution with per-view role, not sure if it's stable enough but here's my checkPermission() version.

 private function checkPermissions()
  {

      $easyAdmin = $this->request->attributes->get('easyadmin');

      if (isset($easyAdmin['entity'][$easyAdmin['view']]['require_permission'])) {
          $requiredPermission = $easyAdmin['entity'][$easyAdmin['view']]['require_permission'];
      } else if(isset($easyAdmin['entity']['require_permission']) ){
          $requiredPermission = $easyAdmin['entity']['require_permission'];
      } else {
          $view = $easyAdmin['view'];
          $entity = $easyAdmin['entity']['name'];
          $requiredPermission = 'ROLE_'.strtoupper($view).'_'.strtoupper($entity);
      }
      $this->denyAccessUnlessGranted(
          $requiredPermission, null, $requiredPermission.' permission required'
      );
  }

Like this I'm still keeping the default ROLE policy but I can define roles this way :

easy_admin:
  entities:
    User:
      class: AppBundle\Entity\User
      label: 'Utilisateurs'
      require_permission: "ROLE_TEST2" # Defaut permission for all views without the require_permission option
      # for new user
      new:
        require_permission: "ROLE_TEST1"
        fields:
         ...
      edit: 
         # No permission here, it'll take the one from User, or ROLE_EDIT_USER if none is defined on User
        actions: ['-delete', '-list']
        ...
Pierstoval commented 7 years ago

@javiereguiluz Can't we use ExpressionLanguage + permission attributes/roles for this ?

We could add two options permission_expression and roles that would be handled manually.

The advantage of permission expression is that we could allow extending it and make it very flexible

luislaborda commented 7 years ago

Is the option of show/hide an entity menu item available by user role? or still to be implemented.

Pierstoval commented 7 years ago

@luislaborda It has to be implemented, but I think @javiereguiluz is not very fond of the idea... Javier? 😉

luislaborda commented 7 years ago

@Pierstoval Thanks, it will be nice having roles because then I can limit views based on roles.

laurent-bientz commented 7 years ago

For french people (sry), if you want to implement some ACL while waiting EasyAdmin 2.0, a quick draft of how we've implemented it in our company (based on @bernardpeh works):

https://github.com/WandiParis/documentation/blob/master/symfony/06-easy-admin-bundle-acl.md

Pierstoval commented 7 years ago

@laurent-bientz VERY interesting! @javiereguiluz You should link this article on the next "A week of Symfony" blog post 😉 And by the way, as everything is based on templates + events + compiler pass, you could propose the very first EasyAdmin extension bundle 😉

bernardpeh commented 7 years ago

I don't know french but nice work @laurent-bientz ... An alternative to using "currentEntityConfig[actionName].role" in the views is to overwrite getActionsForItem as describe in https://leanpub.com/practicalsymfony3/read#leanpub-auto-adding-roles-to-easyadmin-actions

laurent-bientz commented 7 years ago

@guyz, sorry for the french, it was an internal workshop ;)

@bernardpeh thx for your clever idea for overwriting actions in the view side, really great approach! It was for us the worse, crappy and less generic part of this work, i'll give it a try on the next project.

reypm commented 7 years ago

@laurent-bientz is there any chance you translate this into English? I can't understand French and this seems to be a very good work so I want to learn :+1:

laurent-bientz commented 7 years ago

@reypm not now sry, too many work.

Try to follow and adapt works from @bernardpeh, it's really nice documented:

https://leanpub.com/practicalsymfony3/read#leanpub-auto-adding-simple-access-control-to-easyadminbundle

Put your questions here if any problems.

Cheers

djluza commented 6 years ago

I just add Menu with roles. Follow these step:

1)config.yml

    menu:
        - label: 'Test'
          require_permission: ["ROLE_ADMIN", "ROLE_USER"]
          icon: 'users'
          children:
            - { label: 'Menu1', icon: 'file-new', entity: 'Clienti' }
            - { label: 'Menu2', icon: 'file-new', entity: 'Utilizzatori' }

        - label: 'Menu'
          require_permission: ["ROLE_GUEST"]
          icon: 'users'
          children:
            - { label: 'Menu1', icon: 'file-new', entity: 'Clienti' }

2)Template menu.html.twig

3)Thats all!

Pierstoval commented 6 years ago

You don't even need to update the compiler pass, a simple isset() in php or |default([]) in twig makes everything work without more override :D

djluza commented 6 years ago

You are right, thank you for the clarification. 👍

zkkan commented 6 years ago
menu:
        - label: 'Test'
          require_permission: ["ROLE_ADMIN", "ROLE_USER"]
          icon: 'users'
          children:
            - { label: 'Menu1', icon: 'file-new', entity: 'Clienti' }
            - { label: 'Menu2', icon: 'file-new', entity: 'Utilizzatori' }

        - label: 'Menu'
          require_permission: ["ROLE_GUEST"]
          icon: 'users'
      children:
        - { label: 'Menu1', icon: 'file-new', entity: 'Clienti' }

How to restricting action with this implementation? I tried but it doesn't work

djluza commented 6 years ago

You can restrict action in this way:

entities:
        Clienti:
                class: App\xxx\Entity\Clienti
                controller: App\xxx\Controller\ClientiController
                require_permission: ["ROLE_ADMIN","ROLE_USER"]
                list:
                    require_permission: ["ROLE_ADMIN","ROLE_USER"]
                    actions:
                         - { name: 'new', label: 'Crea Nuovo', css_class: 'btn btn-success' }
                    title: 'Clienti'
                    fields:
                        #- { property: 'codcliente', label: 'Codice' }
                        - { property: 'desccliente', label: 'Descrizione' }
                        - { property: 'rifgruppoclienti', label: 'Riferimento Gruppo' }
                edit:
                    require_permission: ["ROLE_ADMIN","ROLE_USER"]
                    title: 'Modifica Clienti'
                    fields:
                        #- { property: 'codcliente', label: 'Codice' }
                        - { property: 'desccliente', label: 'Descrizione' }
                        - { property: 'rifgruppoclienti', label: 'Riferimento Gruppo'}
                new:
                    require_permission: ["ROLE_ADMIN","ROLE_USER"]
                    #actions:
                        #- { type: 'entity', name: 'Gruppiclienti', action: 'new', label: 'User Details' }
                    title: 'Crea Clienti'
                    fields:
                        - { type: 'section', label: 'Test Linea' }
                        #- { property: 'codcliente', label: 'Codice' }
                        - { property: 'desccliente', label: 'Descrizione' }
                        - { property: 'rifgruppoclienti', label: 'Riferimento Gruppo'}
                delete:
                    require_permission: ["ROLE_ADMIN","ROLE_USER"]

Add this code in AdminController.php

private function checkPermissions()
    {
        if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
            throw $this->createAccessDeniedException();
        }

        $easyAdmin = $this->request->attributes->get('easyadmin');

        $action = $this->request->query->get('action');

        $perms = $easyAdmin['entity'][$action]['require_permission'];
        $roles = $this->get('security.context')->getToken()->getUser()->getRoles();

        foreach ($roles as $key => $value)
        {

            $permessi_file = $value;

            if (in_array($permessi_file, $perms)) {

                $requiredPermission = $permessi_file;

            }
            else
            {
                $view = $easyAdmin['view'];
                $entity = $easyAdmin['entity']['name'];
                $requiredPermission = 'ROLE_'.strtoupper($view).'_'.strtoupper($entity);
                # Or any other default strategy
            }
            $this->denyAccessUnlessGranted(
                $requiredPermission, null, $requiredPermission.' permission required'
            );
        }

    }

public function indexAction(Request $request)
    {
        $this->initialize($request);

        if (null === $request->query->get('entity')) {
            return $this->redirectToBackendHomepage();
        }

        $action = $request->query->get('action', 'list');
        if (!$this->isActionAllowed($action)) {
            throw new ForbiddenActionException(array('action' => $action, 'entity_name' => $this->entity['name']));
        }
        $this->checkPermissions();
        return $this->executeDynamicMethod($action.'<EntityName>Action');
    }
forsetius commented 6 years ago

When it comes to restricting the actions, I did it a bit differently:

# /app/config/security
parameters:
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

security:
    role_hierarchy: '%role_hierarchy%'
# User entity definition: user.yml
security:
    role_hierarchy:
        ROLE_USER_SHOW: ~
        ROLE_USER_LIST: ROLE_USER_SEARCH
        ROLE_USER_NEW: ~
        ROLE_USER_EDIT: ~
        ROLE_USER_DELETE: ~
        ROLE_USER_SEE:
            - ROLE_USER_SHOW
            - ROLE_USER_LIST
        ROLE_USER_ALL:
            - ROLE_USER_SEE
            - ROLE_USER_NEW
            - ROLE_USER_EDIT
            - ROLE_USER_DELETE
// ...
use JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController as BaseAdminController;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
// ...

class AdminController extends BaseAdminController
{
    public function indexAction(Request $request)
    {
        // ...
        $r = $request->query;
        if (! $this->hasRole(strtoupper('ROLE_'.$r->get('entity').'_'.$r->get('action'))) ) {
            throw $this->createAccessDeniedException();
        }
    }

    protected $roleHierarchy;
    protected function hasRole($role)
    {
        if (\is_null($this->roleHierarchy)) {
            $this->roleHierarchy = new RoleHierarchy($this->container->getParameter('security.role_hierarchy.roles'));
        }

        return \count($this->roleHierarchy->getReachableRoles([new Role($role)])) > 0;
    }
}

It doesn't require you to write entries for every view separately. Sure, you have to write the hierarchy but once you do it once it is find _ENTITY1NAME_, replace _ENTITY2NAME_. And you have nice role hierarchy you could use to assign roles to groups dynamically :)

Pierstoval commented 6 years ago

@forsetius your solution only takes roles from the role hierarchy system. Using $this->denyAccessUnlessGranted() like the above answer is much better because it allows you to use security attributes instead of roles, which is important when you have voters in your application 😄

zkkan commented 6 years ago

I tried this but it doesn't work. I can't find the error. This is my conf

easy_admin:
    site_name: 'Amitié App'
    formats:
        date:       'd/m/Y'
        time:       'H:i'
        datetime:   'd/m/Y H:i:s'
        number:     '%.2f'
    design:
        menu: 
            - label:                'Members'
              entity:               Member
              icon:                 'users'
              require_permission:   ["ROLE_USER"]

            - label:                'Interventions'
              entity:               Intervention
              icon:                 ambulance
              require_permission:   ["ROLE_USER"]

            - label:                'Intervention type'
              entity:               InterventionType
              icon:                 tags
              require_permission:   ["ROLE_USER"]

            - label:                'Languages'
              entity:               Language
              icon:                 language
              require_permission:   ["ROLE_ADMIN"]

            - label:                'Mental Workers'
              entity:               User
              icon:                 user-circle-o
              require_permission:   ["ROLE_ADMIN"]

            - label:                'Source Of Income'
              entity:               SourceOfIncome
              icon:                 money
              require_permission:   ["ROLE_ADMIN"]

            - label:                'NeighborHood'
              entity:               NeighborHood
              icon:                 street-view
              require_permission:   ["ROLE_ADMIN"]

            - label:                'Housing Arrangement'
              entity:               HousingArrangement
              icon:                 home
              require_permission:   ["ROLE_ADMIN"]

            - label:                'Follow-up Hospital'
              entity:               FollowUpHospital
              icon:                 hospital-o
              require_permission:   ["ROLE_ADMIN"]

            - label:                'Diagnostics'
              entity:               Diagnostic
              icon:                 thermometer-0
              require_permission:   ["ROLE_ADMIN"]

            - label:                'Services'
              entity:               Service
              icon:                 server
              require_permission:   ["ROLE_ADMIN"]

            - label:                'Referred by'
              entity:               ReferredBy
              icon:                 user-plus
              require_permission:   ["ROLE_ADMIN"]

        templates:
            menu: 'AmitiePlatformBundle:Menu:menu.html.twig'
    list:
        actions:
            - { name: 'show', label: 'Show' }
            - { name: 'statistic', label: 'Statistic'}
    entities:
        User:
            class: Amitie\PlatformBundle\Entity\User
            list:
                fields: ['id', 'enabled', 'firstName', 'lastName', 'username' , 'email']
                sort: ['firstName', 'ASC']
            form:
                fields:
                    - { type: 'group', css_class: 'col-sm-6', label: 'Basic information' }
                    - firstName
                    - lastName
                    - username
                    - { property: 'plainPassword', type: 'text', type_options: { required: true } }
                    - email
                    - { type: 'group', css_class: 'col-sm-6', label: 'Others informations' }
                    - usernameCanonical
                    - emailCanonical
                    - roles
                    - enabled
            show: 
                fields: [
                    'id', 
                    'enabled', 
                    'firstName', 
                    'lastName', 
                    'username', 
                    'password', 
                    'email', 
                    'usernameCanonical',
                    'emailCanonical',
                    'roles',
                    'enabled',
                    'interventions', 
                    'lastLogin']

        Member:
            class: Amitie\PlatformBundle\Entity\Member
            #controller: Amitie\PlatformBundle\Controller\MemberController
            list:
                fields: ['id', 'status', 'firstName', 'lastName', 'phone' ,'users']
            show:
                form:
                    fields: 
                        - { type: 'group', css_class: 'col-sm-6', label: 'Basic information' }
                        - lastName

            form:
                fields:
                    - { type: 'group', css_class: 'col-sm-6', label: 'Basic information' }
                    - firstName
                    - lastName
                    - { property: gender, type: choice, type_options: { choices: {"Male": "M", "Female": "F"} } }
                    - birthDay
                    - age
                    - { property: maritalStatus, type: choice, type_options: { choices: {"Célibataire": "C", "Marié": "F", "Divorcé": "D", "Veuf": "V", "Séparé": "S"} } }
                    - diagnostics
                    - language
                    - dateOfEntry
                    - healthInsuranceNumber
                    - status
                    - users
                    - followUpHospital
                    - services
                    - { type: 'group', label: 'Contact information', icon: 'phone',
                        css_class: 'col-sm-6' }
                    - phone
                    - address
                    - city
                    - postalCode
                    - { type: 'group', label: 'Others informations', css_class: 'col-sm-6' }
                    - referredBy
                    - neighborHood
                    - housingArrangement
                    - followUpFrequency
                    - frequencyAtCenter
                    - homelessness
                    - othersLanguages
                    - sourcesOfIncome

        SourceOfIncome:
            class: Amitie\PlatformBundle\Entity\SourceOfIncome
            list:
                fields: ['name']
                sort: ['name', 'ASC']

        NeighborHood:
            class: Amitie\PlatformBundle\Entity\NeighborHood
            list:
                fields: ['name']    
                sort: ['name', 'ASC']

        Language:
            class: Amitie\PlatformBundle\Entity\Language
            list:
                fields: ['name']
                sort: ['name', 'ASC']

        HousingArrangement:
            class: Amitie\PlatformBundle\Entity\HousingArrangement
            list:
                fields: ['name']
                sort: ['name', 'ASC']

        FollowUpHospital:
            class: Amitie\PlatformBundle\Entity\FollowUpHospital
            list:
                fields: ['name']
                sort: ['name', 'ASC']

        Diagnostic:
            class: Amitie\PlatformBundle\Entity\Diagnostic
            list:
                fields: ['name']
                sort: ['name', 'ASC']

        Service:
            class: Amitie\PlatformBundle\Entity\Service
            list:
                fields: ['name']
                sort: ['name', 'ASC']

        ReferredBy:
            class: Amitie\PlatformBundle\Entity\ReferredBy
            list:
                fields: ['name']
                sort: ['name', 'ASC']

        Intervention:
            controller: Amitie\PlatformBundle\Controller\InterventionController
            class: Amitie\PlatformBundle\Entity\Intervention
            list:
                fields: ['member', 'date', 'type', 'duration', 'createdBy']
                sort: ['date', 'DESC']
            form:
                fields:
                    - { type: 'group', css_class: 'col-sm-6', label: 'Basic information' }

        InterventionType:
            class: Amitie\PlatformBundle\Entity\InterventionType
            list:
                fields: ['name']
                sort: ['name', 'ASC']
Pierstoval commented 6 years ago

@zkkan can you put your code in a gist or pastebin, and also show the code from your custom admin controller?

zkkan commented 6 years ago

These are my code

easy_admin_bundle.yml: https://pastebin.com/PCM4JzB0 security.yml: https://pastebin.com/QY9t1LVr AdminController: https://pastebin.com/vqR4Q06U

I have 2 roles: ROLE_ADMIN, ROLE_USER

What I want to do is restricting some actions when login as user. example: Deleting a member, see some field of member etc

Thanks

Pierstoval commented 6 years ago

@zkkan Do you understand that the require_permission is a parameter that was proposed, but in fact it is not implemented in EasyAdmin? This is the reason why it doesn't work.

zkkan commented 6 years ago

@Pierstoval Are there a way to do this?

Pierstoval commented 6 years ago

Yes, follow the other recommendations in this thread, like the one by @djluza for example

javiereguiluz commented 6 years ago

@zkkan currently there is no official way to implement that feature with this bundle. If you need this feature and must develop your project soon, the only solution is to use instead this other bundle: SonataAdminBundle.

In any case, it's going to take us a bit before we complete this feature, so I'm going to lock this issue to prevent people adding more comments and asking about this. Thanks!