Closed reypm closed 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!
@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?
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?
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.
@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?
This code app.user.roles|default([])|join(' ')|lower
should make your <body>
look like this:
<body ... class="... role_admin role_user" ... >
@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.
I haven't tested this, but you could start as follows:
role
(or roles
to support several) to everything in EasyAdmin: menu items, entities, properties.role
option should survive our config processing. So you have the role
available in every template.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) }}
...
@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
@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:
role
property for everything in the backend (menus, actions, links, buttons, properties, controller method, etc.)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 ;)
@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.
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.
@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 ;)
@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
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
@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.
@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.
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
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
@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".
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.
@Glideh I like your solution! @javiereguiluz , could it be documented/implemented?
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
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']
....
@bernardpeh Thanks man, you saved my day
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']
...
@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
Is the option of show/hide an entity menu item available by user role? or still to be implemented.
@luislaborda It has to be implemented, but I think @javiereguiluz is not very fond of the idea... Javier? 😉
@Pierstoval Thanks, it will be nice having roles because then I can limit views based on roles.
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
@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 😉
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
@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.
@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:
@reypm not now sry, too many work.
Try to follow and adapt works from @bernardpeh, it's really nice documented:
Put your questions here if any problems.
Cheers
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!
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
You are right, thank you for the clarification. 👍
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
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');
}
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 :)
@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 😄
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']
@zkkan can you put your code in a gist or pastebin, and also show the code from your custom admin controller?
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
@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.
@Pierstoval Are there a way to do this?
Yes, follow the other recommendations in this thread, like the one by @djluza for example
@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!
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?