Browse Source

ACL: the form is updated to allow to edit roles the user is MASTER for and display the rest in a readonly list + the user list is updated: do not allow to edit links for superadmins if the user is not a super admin

Roel Sint 13 years ago
parent
commit
392915f1e0

+ 2 - 1
Admin/Entity/UserAdmin.php

@@ -16,6 +16,7 @@ use Sonata\AdminBundle\Form\FormMapper;
 use Sonata\AdminBundle\Datagrid\DatagridMapper;
 use Sonata\AdminBundle\Datagrid\ListMapper;
 use Sonata\AdminBundle\Route\RouteCollection;
+use Sonata\AdminBundle\Security\Acl\Permission\MaskBuilder;
 
 use FOS\UserBundle\Model\UserManagerInterface;
 
@@ -28,7 +29,7 @@ class UserAdmin extends Admin
     protected function configureListFields(ListMapper $listMapper)
     {
         $listMapper
-            ->addIdentifier('username')
+            ->addIdentifier('username', null, array('template' => 'SonataUserBundle:UserAdmin:list_field_username.html.twig'))
             ->add('email')
             ->add('enabled')
             ->add('locked')

+ 1 - 0
DependencyInjection/SonataUserExtension.php

@@ -11,6 +11,7 @@
 
 namespace Sonata\UserBundle\DependencyInjection;
 
+use Symfony\Component\Config\Definition\Processor;
 use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
 use Symfony\Component\Config\Resource\FileResource;
 use Symfony\Component\DependencyInjection\ContainerBuilder;

+ 65 - 10
Form/Type/SecurityRolesType.php

@@ -13,6 +13,9 @@
 namespace Sonata\UserBundle\Form\Type;
 
 use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\FormView;
 use Sonata\AdminBundle\Admin\Pool;
 
 class SecurityRolesType extends ChoiceType
@@ -24,12 +27,43 @@ class SecurityRolesType extends ChoiceType
         $this->pool = $pool;
     }
 
+    /**
+     * {@inheritdoc}
+     */
+    public function buildForm(FormBuilder $builder, array $options)
+    {
+        parent::buildForm($builder, $options);
+
+        $builder->setAttribute('read_only_choices', $options['read_only_choices']);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function buildView(FormView $view, FormInterface $form)
+    {
+        parent::buildView($view, $form);
+
+        $attr = $view->get('attr', array());
+
+        if (isset($attr['class']) && empty($attr['class'])) {
+            $attr['class'] = 'sonata-medium';
+        }
+
+        $view->set('attr', $attr);
+
+        $view->set('read_only_choices', $form->getAttribute('read_only_choices'));
+    }
+
     public function getDefaultOptions(array $options)
     {
         $options = parent::getDefaultOptions($options);
 
         $roles = array();
+        $rolesReadOnly = array();
         if (count($options['choices']) == 0) {
+            $securityContext = $this->pool->getContainer()->get('security.context');
+
             // get roles from the Admin classes
             foreach ($this->pool->getAdminServiceIds() as $id) {
                 try {
@@ -38,28 +72,49 @@ class SecurityRolesType extends ChoiceType
                     continue;
                 }
 
+                $isMaster = $admin->isGranted('MASTER');
                 $securityHandler = $admin->getSecurityHandler();
+                // TODO get the base role from the admin or security handler
+                $baseRole = $securityHandler->getBaseRole($admin);
 
-                foreach ($securityHandler->buildSecurityInformation($admin) as $role => $acls) {
-                    $roles[$role] = $role;
+                foreach ($admin->getSecurityInformation() as $role => $permissions) {
+                    $role = sprintf($baseRole, $role);
+                    if ($isMaster) {
+                        // if the user has the MASTER permission, allow to grant access the admin roles to other users
+                        $roles[$role] = $role;
+                    } elseif ($securityContext->isGranted($role)) {
+                        // although the user has no MASTER permission, allow the currently logged in user to view the role
+                        $rolesReadOnly[$role] = $role;
+                    }
                 }
             }
 
             // get roles from the service container
             foreach ($this->pool->getContainer()->getParameter('security.role_hierarchy.roles') as $name => $rolesHierarchy) {
-                $roles[$name] = $name . ': ' . implode(', ', $rolesHierarchy);
-                
-                foreach ($rolesHierarchy as $role) {
-                    if (!isset($roles[$role])) {
-                        $roles[$role] = $role;
+
+                if ($securityContext->isGranted($name)) {
+                    $roles[$name] = $name . ': ' . implode(', ', $rolesHierarchy);
+
+                    foreach ($rolesHierarchy as $role) {
+                        if (!isset($roles[$role])) {
+                            $roles[$role] = $role;
+                        }
                     }
                 }
-                
             }
-
-            $options['choices'] = $roles;
         }
 
+        $options['choices'] = $roles;
+        $options['read_only_choices'] = $rolesReadOnly;
+
         return $options;
     }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'sonata_security_roles';
+    }
 }

+ 22 - 0
Resources/config/security_acl.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<container xmlns="http://symfony.com/schema/dic/services"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+
+    <parameters>
+        <parameter key="security.acl.user_voter.class">Sonata\UserBundle\Security\Authorization\Voter\UserAclVoter</parameter>
+    </parameters>
+
+    <services>
+        <service id="security.acl.voter.user_permissions" class="%security.acl.user_voter.class%" public="false">
+            <tag name="monolog.logger" channel="security" />
+            <argument type="service" id="security.acl.provider" />
+            <argument type="service" id="security.acl.object_identity_retrieval_strategy" />
+            <argument type="service" id="security.acl.security_identity_retrieval_strategy" />
+            <argument type="service" id="security.acl.permission.map" />
+            <argument type="service" id="logger" on-invalid="null" />
+            <tag name="security.voter" priority="255" />
+        </service>
+    </services>    
+</container>

+ 7 - 0
Resources/doc/reference/installation.rst

@@ -209,3 +209,10 @@ The last part is to define 3 new access control rules :
             # Change these rules to adapt them to your needs
             - { path: ^/admin, role: [ROLE_ADMIN, ROLE_SONATA_ADMIN] }
             - { path: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
+
+
+Using the roles
+---------------------------------------------------
+
+Each admin has its own roles, use the user form to assign them to other users. The available roles to assign to others
+are limited to the roles available to the user editing the form.

+ 18 - 0
Resources/views/Form/form_admin_fields.html.twig

@@ -0,0 +1,18 @@
+{% block sonata_security_roles_widget %}
+{% spaceless %}
+    <div class="editable">
+        <h4>{{ 'field.label_roles_editable'|trans }}</h4>
+        {{ block('choice_widget') }}
+    </div>
+    {% if read_only_choices|length > 0 %}
+    <div class="readonly">
+        <h4>{{ 'field.label_roles_readonly'|trans }}</h4>
+        <ul>
+        {% for choice in read_only_choices %}
+            <li>{{ choice }}</li>
+        {% endfor %}
+        </ul>
+    </div>
+    {% endif %}
+{% endspaceless %}
+{% endblock sonata_security_roles_widget %}

+ 27 - 0
Resources/views/UserAdmin/list_field_username.html.twig

@@ -0,0 +1,27 @@
+{#
+
+This file is part of the Sonata package.
+
+(c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+
+For the full copyright and license information, please view the LICENSE
+file that was distributed with this source code.
+
+#}
+
+<td class="sonata-ba-list-field sonata-ba-list-field-{{ field_description.type }}" objectId="{{ admin.id(object) }}">
+    {% if field_description.options.identifier is defined and admin.isGranted('VIEW', object) %}
+
+        {% if admin.hasroute('edit') and admin.isGranted('EDIT', object) %}
+            <a href="{{ admin.generateObjectUrl('edit', object) }}">
+        {% elseif admin.hasroute('show') and admin.show|length > 0 %}
+            <a href="{{ admin.generateObjectUrl('show', object) }}">
+        {% endif %}
+
+            {% block field %}{{ value }}{% endblock %}
+        </a>
+
+    {% else %}
+        {{ block('field') }}
+    {% endif %}
+</td>

+ 43 - 0
Security/Authorization/Voter/UserAclVoter.php

@@ -0,0 +1,43 @@
+<?php
+namespace Sonata\UserBundle\Security\Authorization\Voter;
+
+use FOS\UserBundle\Model\UserInterface;
+
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Acl\Voter\AclVoter;
+
+class UserAclVoter extends AclVoter
+{
+   /**
+    * {@InheritDoc}
+    */
+   public function supportsClass($class)
+   {
+       // support the Object-Scope ACL
+       return is_subclass_of($class, 'FOS\UserBundle\Model\UserInterface');
+   }
+
+   public function supportsAttribute($attribute)
+   {
+       return $attribute === 'EDIT' || $attribute === 'DELETE';
+   }
+
+   public function vote(TokenInterface $token, $object, array $attributes)
+   {
+       if (!$this->supportsClass(get_class($object))) {
+           return self::ACCESS_ABSTAIN;
+       }
+
+       foreach ($attributes as $attribute) {
+           if ($this->supportsAttribute($attribute) && $object instanceof UserInterface) {
+               if ($object->isSuperAdmin() && !$token->getUser()->isSuperAdmin()) {
+                   // deny a non super admin user to edit or delete a super admin user
+                   return self::ACCESS_DENIED;
+               }
+           }
+       }
+
+       // leave the permission voting to the AclVoter that is using the default permission map
+       return self::ACCESS_ABSTAIN;
+   }
+}