This bundle has been on my projects for a while and I love it. Since the team behind it still adding new features time to time then I should ask (and I know this has been discussed on the past): is there any plan to integrate FOSUser? I mean what about permissions? ACLs? That's the only thing I miss from others like Sonata and I think will be very helpful for developers and end users? I know FOSUser status for Symfony 2/3 is unknow and I know the team is working hard to get some bugs fixed and makeup the application but as I said it lacks this integration. What do you think on add this as soon as possible?
My answer would be: yes ... but not yet. If someone is thinking about sending a pull request for this, please don't do it. Thanks!
@javiereguiluz I was researching by then but I've found a bit hard so my next question would be and based on your answer: _yes ... but not yet_, without say ETA or something like that, is sooner? future? (+5 months)? "No rush" but I think it's a "must be" and I need to clear out my ideas on when this will be done in order to estimate times and other on the project I am working ;)
A role based config option for this would be cool. Something like this:
easy_admin:
entities:
BlogEntry:
class: AppBundle\Entity\BlogEntry
permissions:
list: ['ROLE_ADMIN', 'ROLE_EDITOR']
create: ['ROLE_ADMIN']
edit: ['ROLE_ADMIN', 'ROLE_EDITOR']
delete: ['ROLE_ADMIN']
list:
fields:
- 'id'
- 'title'
form:
fields:
- 'title'
Hi guys! This can do it for yourself without much effort in just three steps.
Step 1: Overwrite the EasyAdmin AdminController and overwrite the indexAction method in you custom AdminController.
Step 2: Copy and Paste the parent code from this method indexAction
Step 3: Add before return $this->executeDynamicMethod($action.'<EntityName>Action'); the follows statements:
if (isset($this->entity['permissions'][$action])) {
$this->denyAccessUnlessGranted($this->entity['permissions'][$action]);
}
namespace AppBundle\Controller;
use JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController as EasyAdminController;
class AdminController extends EasyAdminController
{
/**
* @Route("/", name="easyadmin")
*
* @param Request $request
*
* @return RedirectResponse|Response
*/
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']));
}
if (isset($this->entity['permissions'][$action])) {
$this->denyAccessUnlessGranted($this->entity['permissions'][$action]);
}
return $this->executeDynamicMethod($action.'<EntityName>Action');
}
}
I'm going to close this issue for these reasons:
I really hope you all understand. Thanks!
@yceruto thanks for this simple solution I will give it a try as soon as I can but as you can see @javiereguiluz wants to do this on his own at some point so we (community) was advise around not to send PR or code or so on and can't do more about. But yes, on the meantime I will use your "patch" could you drop off your email or contact in here? I am from your country as well and will be nice have you in my contacts ;)
I don't think any solution should be provided by EasyAdminBundle itself; but instead show some examples on how to achieve such a thing by exposing code samples in the easy admin demo application.
The solution provided by @yceruto is exactly what I thought about this "feature" (but instead of overriding the AdminController, use a listener on one of the events dispatched by the EasyAdminBundle.)
@ogizanagi I was asking for a solution because this is included in Sonata as part of the whole environment so why no include this in EasyAdmin? Anyway an example as @yceruto did works for any developer so if this is the final decision the team should write a few ones, for now I will test the example on this issue and give feedback to its author. @javiereguiluz toughs?
Just a note that _redirectToBackendHomepage_ and _executeDynamicMethod_ are private methods, so for now I end up overwriting the isActionAllowed method.
For menu roles I follow #1040 .
First I don't believe the responsibility for handle the security must be a feature of the EasyAdminBundle.
EasyAdminBundle do whatever is designed to do: generate a simple backend. Keeping things simple is the best idea. Right now Sonata is a pain in the bot, too many components and the support for compatibility sucks.
ACL Symfony Component Works perfect with EasyAdmin, but i don't like the performance of the ACL voter.
I developed my own component but as a patch for my current project, using a voter and an event subscriber, cuz create new controllers for each entity only to use the security manager and call parent::action() is a waste of time and code.
I'll release that patch as a component very soon (when my current project ends), was a quick solution and need a few touches.
Finally I wanna thanks @javiereguiluz for sharing this amazing bundle, his work and effort, but I dislike totally his attitude about Pull Request (maybe the way he did it)... Everybody is free to fork this project and implement their own patches.
Anyway keep things simple, an independent component is the best idea, and never try to reinvent the wheel.
Using the @ama-gi configuration.
services:
fluency_scheduler.easyadmin.security:
class: Fluency\Bundle\SchedulerBundle\Security\Event\EasyAdminSecurityEventSubscriber
arguments: ['@security.access.decision_manager', '@security.token_storage']
tags:
- { name: kernel.event_subscriber }
<?php
namespace Fluency\Bundle\SchedulerBundle\Security\Event;
use JavierEguiluz\Bundle\EasyAdminBundle\Event\EasyAdminEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class EasyAdminSecurityEventSubscriber implements EventSubscriberInterface
{
private $decisionManager;
private $token;
public function __construct(AccessDecisionManagerInterface $decisionManager, TokenStorageInterface $token)
{
$this->decisionManager = $decisionManager;
$this->token = $token;
}
public static function getSubscribedEvents()
{
return array(
EasyAdminEvents::PRE_LIST => array('isAuthorized'),
EasyAdminEvents::PRE_EDIT => array('isAuthorized'),
EasyAdminEvents::PRE_DELETE => array('isAuthorized'),
EasyAdminEvents::PRE_NEW => array('isAuthorized'),
);
}
public function isAuthorized(GenericEvent $event)
{
$entityConfig = $event['entity'];
$action = $event->getArgument('request')->query->get('action');
$authorizedRoles = $entityConfig['permissions'][$action];
if (!$this->decisionManager->decide($this->token->getToken(), $authorizedRoles)) {
throw new AccessDeniedException();
};
}
}
Inspired by https://github.com/EasyCorp/EasyAdminBundle/issues/807#issuecomment-310644895 and @rernesto in https://github.com/EasyCorp/EasyAdminBundle/issues/1076#issuecomment-300808750 I modifed and combined the codes with the following updates / features:
services:
app.easyadmin.security:
class: App\Security\EasyAdminSecurityEventSubscriber
arguments: ['@security.access.decision_manager', '@security.token_storage']
tags:
- { name: kernel.event_subscriber }
<?php
namespace App\Security;
use JavierEguiluz\Bundle\EasyAdminBundle\Event\EasyAdminEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class EasyAdminSecurityEventSubscriber implements EventSubscriberInterface
{
private $decisionManager;
private $token;
public function __construct(AccessDecisionManagerInterface $decisionManager, TokenStorageInterface $token)
{
$this->decisionManager = $decisionManager;
$this->token = $token;
}
public static function getSubscribedEvents()
{
return array(
EasyAdminEvents::PRE_LIST => array('isAuthorized'),
EasyAdminEvents::PRE_EDIT => array('isAuthorized'),
EasyAdminEvents::PRE_DELETE => array('isAuthorized'),
EasyAdminEvents::PRE_NEW => array('isAuthorized'),
EasyAdminEvents::PRE_SHOW => array('isAuthorized'),
);
}
public function isAuthorized(GenericEvent $event)
{
$entityConfig = $event['entity'];
$action = $event->getArgument('request')->query->get('action');
if (!array_key_exists('permissions', $entityConfig) or !array_key_exists($action, $entityConfig['permissions'])) {
return;
}
$authorizedRoles = $entityConfig['permissions'][$action];
if (!$this->decisionManager->decide($this->token->getToken(), $authorizedRoles)) {
throw new AccessDeniedException();
};
}
}
{# Location: templates/bundles/EasyAdminBundle/default/menu.html.twig #}
{% macro render_menu_item(item, translation_domain) %}
{% if item.type == 'divider' %}
{{ item.label|trans(domain = translation_domain) }}
{% else %}
{% set menu_params = { menuIndex: item.menu_index, submenuIndex: item.submenu_index } %}
{% set path =
item.type == 'link' ? item.url :
item.type == 'route' ? path(item.route, item.params) :
item.type == 'entity' ? path('easyadmin', { entity: item.entity, action: 'list' }|merge(menu_params)|merge(item.params)) :
item.type == 'empty' ? '#' : '' %}
{# if the URL generated for the route belongs to the backend, regenerate
the URL to include the menu_params to display the selected menu item
(this is checked comparing the beginning of the route URL with the backend homepage URL)
#}
{% if item.type == 'route' and (path starts with path('easyadmin')) %}
{% set path = path(item.route, menu_params|merge(item.params)) %}
{% endif %}
<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(domain = translation_domain) }}</span>
{% if item.children|default([]) is not empty %}<i class="fa fa-angle-left pull-right"></i>{% endif %}
</a>
{% endif %}
{% endmacro %}
{% import _self as helper %}
{% block main_menu_before %}{% endblock %}
<ul class="sidebar-menu">
{% set _menu_items = easyadmin_config('design.menu') %}
{% set _roles = app.user.roles %}
{% block main_menu %}
{% for item in _menu_items %}
{% set _denied = true %}
{% set _permissions = easyadmin_config('entities.' ~ item.entity ~ '.permissions') %}
{% for role in _roles if _denied %}
{% if (_permissions.list is not defined) or (role in _permissions.list|default([])) %}
{% set _denied = false %}
{% block menu_item %}
<li class="{{ item.type == 'divider' ? 'header' }} {{ item.children is not empty ? 'treeview' }} {{ app.request.query.get('menuIndex')|default(-1) == loop.index0 ? 'active' }} {{ app.request.query.get('submenuIndex')|default(-1) != -1 ? 'submenu-active' }}">
{{ helper.render_menu_item(item, _entity_config.translation_domain|default('messages')) }}
{% if item.children|default([]) is not empty %}
<ul class="treeview-menu">
{% for subitem in item.children %}
{% block menu_subitem %}
<li class="{{ subitem.type == 'divider' ? 'header' }} {{ app.request.query.get('menuIndex')|default(-1) == loop.parent.loop.index0 and app.request.query.get('submenuIndex')|default(-1) == loop.index0 ? 'active' }}">
{{ helper.render_menu_item(subitem, _entity_config.translation_domain|default('messages')) }}
</li>
{% endblock menu_subitem %}
{% endfor %}
</ul>
{% endif %}
</li>
{% endblock menu_item %}
{% endif %}
{% endfor %}
{% endfor %}
{% endblock main_menu %}
</ul>
{% block main_menu_after %}{% endblock %}
{# Location: templates/bundles/EasyAdminBundle/default/includes/_actions.html.twig #}
{% set _roles = app.user.roles %}
{% set _permissions = easyadmin_config('entities.' ~ app.request.query.get('entity') ~ '.permissions') %}
{% for action in actions %}
{% set _denied = true %}
{% for role in _roles if _denied %}
{% if (_permissions[action.name] is not defined) or (role in _permissions[action.name]|default([])) %}
{% set _denied = false %}
{% if 'list' == action.name %}
{% set action_href = request_parameters.referer|default('') ? request_parameters.referer|easyadmin_urldecode : path('easyadmin', request_parameters|merge({ action: 'list' })) %}
{% elseif 'method' == action.type %}
{% set action_href = path('easyadmin', request_parameters|merge({ action: action.name, id: item_id })) %}
{% elseif 'route' == action.type %}
{% set action_href = path(action.name, request_parameters|merge({ action: action.name, id: item_id })) %}
{% endif %}
<a class="{{ action.css_class|default('') }}"
title="{{ action.title|default('') is empty ? '' : action.title|trans(trans_parameters, translation_domain) }}"
href="{{ action_href }}" target="{{ action.target }}">
{%- if action.icon %}<i class="fa fa-{{ action.icon }}"></i> {% endif -%}
{%- if action.label is defined and not action.label is empty -%}
{{ action.label|trans(arguments = trans_parameters|merge({ '%entity_id%': item_id }), domain = translation_domain) }}
{%- endif -%}
</a>
{% endif %}
{% endfor %}
{% endfor %}
Config (example):
entities:
PDF:
class: App\Entity\PDF
permissions:
list: ['ROLE_SUPER_ADMIN']
new: ['ROLE_SUPER_ADMIN']
edit: ['ROLE_SUPER_ADMIN']
delete: ['ROLE_SUPER_ADMIN']
show: ['ROLE_SUPER_ADMIN']
form:
fields:
- { property: 'name' }
- { property: 'url' }
list:
actions: ['show']
Hopefully that could help anyone, for me its fitting all my need so far.
Most helpful comment
Inspired by https://github.com/EasyCorp/EasyAdminBundle/issues/807#issuecomment-310644895 and @rernesto in https://github.com/EasyCorp/EasyAdminBundle/issues/1076#issuecomment-300808750 I modifed and combined the codes with the following updates / features:
Config (example):
Hopefully that could help anyone, for me its fitting all my need so far.