Browse Source

Start integrating the security component, small tweak

Thomas Rabaix 14 years ago
parent
commit
4401f34984

+ 18 - 17
Builder/ORM/FormContractor.php

@@ -38,21 +38,21 @@ class FormContractor implements FormContractorInterface
      * @var array
      */
     protected $formTypes = array(
-        'string'     =>  'text',
-        'text'       =>  'textarea',
-        'boolean'    =>  'checkbox',
-        'checkbox'   =>  'checkbox',
-        'integer'    =>  'integer',
-        'tinyint'    =>  'integer',
-        'smallint'   =>  'integer',
-        'mediumint'  =>  'integer',
-        'bigint'     =>  'integer',
-        'decimal'    =>  'number',
-        'datetime'   =>  'datetime',
-        'date'       =>  'date',
-        'choice'     =>  'choice',
-        'array'      =>  'collection',
-        'country'    =>  'country',
+        'string'     =>  array('text', array()),
+        'text'       =>  array('textarea', array()),
+        'boolean'    =>  array('checkbox', array()),
+        'checkbox'   =>  array('checkbox', array()),
+        'integer'    =>  array('integer', array()),
+        'tinyint'    =>  array('integer', array()),
+        'smallint'   =>  array('integer', array()),
+        'mediumint'  =>  array('integer', array()),
+        'bigint'     =>  array('integer', array()),
+        'decimal'    =>  array('number', array()),
+        'datetime'   =>  array('datetime', array()),
+        'date'       =>  array('date', array()),
+        'choice'     =>  array('choice', array()),
+        'array'      =>  array('collection', array()),
+        'country'    =>  array('country', array()),
     );
 
     public function __construct(FormFactoryInterface $formFactory)
@@ -231,10 +231,11 @@ class FormContractor implements FormContractorInterface
                 break;
 
             default:
+                list($type, $default_options) = $this->getFormTypeName($fieldDescription);
                 $formBuilder->add(
                     $fieldDescription->getFieldName(),
-                    $this->getFormTypeName($fieldDescription),
-                    $fieldDescription->getOption('form_field_options', array())
+                    $type,
+                    array_merge($default_options, $fieldDescription->getOption('form_field_options', array()))
                 );
         }
     }

+ 118 - 0
Command/DumpActionRolesCommand.php

@@ -0,0 +1,118 @@
+<?php
+
+/*
+ * 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.
+ */
+
+namespace Sonata\AdminBundle\Command;
+
+use Symfony\Bundle\FrameworkBundle\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Output\Output;
+
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\Config\Resource\FileResource;
+
+class DumpActionRolesCommand extends Command
+{
+
+    public function configure()
+    {
+        $this->setName('sonata:admin:dump-action-roles');
+        $this->setDescription('');
+        $this->addOption('format', null, InputOption::VALUE_OPTIONAL, 'define the output format (default: yaml)', 'yaml');
+        $this->addOption('prefix', null, InputOption::VALUE_OPTIONAL, 'define the admin route prefix (default: /admin)', '/admin');
+    }
+
+    public function execute(InputInterface $input, OutputInterface $output)
+    {
+        $infos = array();
+        foreach ($this->getAdminRoutesCollection($input->getOption('prefix'))->all() as $route) {
+            $compiledRoute = $route->compile();
+
+            $regex = str_replace(array("\n", ' '), '', $compiledRoute->getRegex());
+            if ($pos = strpos($regex, '/$')) {
+                $regex = substr($regex, 0, $pos).'/?$'.substr($regex, $pos + 2);
+            }
+
+            $defaults = $route->getDefaults();
+
+            $controllerInfos = explode(':', $defaults['_controller']);
+
+            $group = strtoupper(sprintf('ROLE_%s', str_replace(array('.','|'), '_', strtoupper($defaults['_sonata_admin']))));
+            if (!isset($infos[$group])) {
+                $infos[$group] = array();
+            }
+
+            $name = strtoupper(sprintf('ROLE_%s_%s',
+                str_replace(array('.','|'), '_', strtoupper($defaults['_sonata_admin'])),
+                $controllerInfos[2]
+            ));
+
+            $infos[$group][] = array(
+                'path' => substr($regex, 1, -2),
+                'roles' => $name
+            );
+        }
+
+        $this->dumpYaml($output, $infos);
+    }
+
+    public function dumpYaml(OutputInterface $output, array $infos)
+    {
+
+        $output->writeln('sonata_admin:');
+        $output->writeln('    access_control:');
+        foreach ($infos as $groups) {
+            foreach ($groups as $group) {
+                $output->writeln(sprintf('        - { path: %s, roles: [%s], methods: null }', $group['path'], $group['roles']));
+            }
+        }
+
+        $output->writeln('');
+        $output->writeln('    role_hierarchy:');
+
+        $superAdmin = array();
+        foreach ($infos as $groupName => $groups) {
+            $roles = array();
+            foreach ($groups as $group) {
+                $roles[] = $group['roles'];
+            }
+            $output->writeln(sprintf('        %s: [%s] ', $groupName, implode(', ', $roles)));
+
+            $superAdmin[] = $groupName;
+        }
+
+        $output->writeln(sprintf('        ROLE_SONATA_ADMIN_ROOT: [%s] ', implode(', ', $superAdmin)));
+    }
+
+    public function getAdminRoutesCollection($prefix)
+    {
+        $pool = $this->container->get('sonata.admin.pool');
+        $collection = new RouteCollection;
+
+        foreach ($pool->getAdminServiceIds() as $id) {
+
+            $admin = $pool->getInstance($id);
+
+            foreach ($admin->getRoutes()->getElements() as $code => $route) {
+                $collection->add($route->getDefault('_sonata_name'), $route);
+            }
+
+            $reflection = new \ReflectionObject($admin);
+            $collection->addResource(new FileResource($reflection->getFileName()));
+        }
+
+        $collection->addPrefix($prefix);
+        return $collection;
+    }
+}

+ 101 - 0
DependencyInjection/AddSecurityCallsPass.php

@@ -0,0 +1,101 @@
+<?php
+
+/*
+ * This file is part of the Sonata project.
+ *
+ * (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.
+ */
+
+namespace Sonata\AdminBundle\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * This code append Admin security roles
+ *
+ * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class AddSecurityCallsPass implements CompilerPassInterface
+{
+    /**
+     * {@inheritDoc}
+     */
+    public function process(ContainerBuilder $container)
+    {
+        $definition = $container->getDefinition('sonata_dummy_security');
+        $container->removeDefinition('sonata_dummy_security');
+
+        $config = $definition->getArguments();
+
+        $this->createAuthorization($config, $container);
+        $this->createRoleHierarchy($config, $container);
+    }
+
+    private function createRoleHierarchy($config, ContainerBuilder $container)
+    {
+        if (!isset($config['role_hierarchy'])) {
+            $container->removeDefinition('security.access.role_hierarchy_voter');
+
+            return;
+        }
+
+        $parameters = (array) $container->getParameter('security.role_hierarchy.roles');
+
+        $container->setParameter('security.role_hierarchy.roles', array_merge($parameters, $config['role_hierarchy']));
+        $container->removeDefinition('security.access.simple_role_voter');
+    }
+
+    private function createAuthorization($config, ContainerBuilder $container)
+    {
+        if (!$config['access_control']) {
+            return;
+        }
+
+        foreach ($config['access_control'] as $access) {
+            $matcher = $this->createRequestMatcher(
+                $container,
+                $access['path'],
+                $access['host'],
+                count($access['methods']) === 0 ? null : $access['methods'],
+                $access['ip']
+            );
+
+            $container->getDefinition('security.access_map')
+                      ->addMethodCall('add', array($matcher, $access['roles'], $access['requires_channel']));
+        }
+    }
+
+    private function createRequestMatcher($container, $path = null, $host = null, $methods = null, $ip = null, array $attributes = array())
+    {
+        $serialized = serialize(array($path, $host, $methods, $ip, $attributes));
+        $id = 'security.request_matcher.'.md5($serialized).sha1($serialized);
+
+        if (isset($this->requestMatchers[$id])) {
+            return $this->requestMatchers[$id];
+        }
+
+        // only add arguments that are necessary
+        $arguments = array($path, $host, $methods, $ip, $attributes);
+        while (count($arguments) > 0 && !end($arguments)) {
+            array_pop($arguments);
+        }
+
+        $container
+            ->register($id, '%security.matcher.class%')
+            ->setPublic(false)
+            ->setArguments($arguments)
+        ;
+
+        return $this->requestMatchers[$id] = new Reference($id);
+    }
+
+}

+ 63 - 4
DependencyInjection/Configuration.php

@@ -11,10 +11,10 @@
 
 namespace Sonata\AdminBundle\DependencyInjection;
 
-use Symfony\Component\Config\Definition\Builder;
-
-use Symfony\Component\Config\Definition\Builder\NodeBuilder;
 use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
 
 /**
  * This class contains the configuration information for the bundle
@@ -36,6 +36,15 @@ class Configuration
         $treeBuilder = new TreeBuilder();
         $rootNode = $treeBuilder->root('sonata_admin', 'array');
 
+        $this->addTemplateSection($rootNode);
+        $this->addAccessControlSection($rootNode);
+        $this->addRoleHierarchySection($rootNode);
+
+        return $treeBuilder->buildTree();
+    }
+
+    private function addTemplateSection(ArrayNodeDefinition $rootNode)
+    {
         $rootNode
             ->children()
                 ->arrayNode('templates')
@@ -46,7 +55,57 @@ class Configuration
                 ->end()
             ->end()
         ->end();
+    }
 
-        return $treeBuilder->buildTree();
+    private function addAccessControlSection(ArrayNodeDefinition $rootNode)
+    {
+        $rootNode
+            ->fixXmlConfig('rule', 'access_control')
+            ->children()
+                ->arrayNode('access_control')
+                    ->cannotBeOverwritten()
+                    ->prototype('array')
+                        ->children()
+                            ->scalarNode('requires_channel')->defaultNull()->end()
+                            ->scalarNode('path')->defaultNull()->end()
+                            ->scalarNode('host')->defaultNull()->end()
+                            ->scalarNode('ip')->defaultNull()->end()
+                            ->arrayNode('methods')
+                                ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
+                                ->prototype('scalar')->end()
+                            ->end()
+                        ->end()
+                        ->fixXmlConfig('role')
+                        ->children()
+                            ->arrayNode('roles')
+                                ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
+                                ->prototype('scalar')->end()
+                            ->end()
+                        ->end()
+                    ->end()
+                ->end()
+            ->end()
+        ;
+    }
+
+    private function addRoleHierarchySection(ArrayNodeDefinition $rootNode)
+    {
+        $rootNode
+            ->fixXmlConfig('role', 'role_hierarchy')
+            ->children()
+                ->arrayNode('role_hierarchy')
+                    ->useAttributeAsKey('id')
+                    ->prototype('array')
+                        ->performNoDeepMerging()
+                        ->beforeNormalization()->ifString()->then(function($v) { return array('value' => $v); })->end()
+                        ->beforeNormalization()
+                            ->ifTrue(function($v) { return is_array($v) && isset($v['value']); })
+                            ->then(function($v) { return preg_split('/\s*,\s*/', $v['value']); })
+                        ->end()
+                        ->prototype('scalar')->end()
+                    ->end()
+                ->end()
+            ->end()
+        ;
     }
 }

+ 4 - 0
DependencyInjection/SonataAdminExtension.php

@@ -37,6 +37,8 @@ class SonataAdminExtension extends Extension
         )
     );
 
+    protected $requestMatchers = array();
+
     /**
      *
      * @param array            $configs    An array of configuration settings
@@ -57,6 +59,8 @@ class SonataAdminExtension extends Extension
 
         // setups parameters with values in config.yml, default values from external files used if not
         $this->configSetupTemplates($config, $container);
+
+        $container->setDefinition('sonata_dummy_security', new Definition('stdClass', $config));
     }
 
     protected function configSetupTemplates($config, $container)

+ 11 - 0
Form/FormMapper.php

@@ -190,4 +190,15 @@ class FormMapper
     {
         return $this->admin;
     }
+
+    /**
+     * @param string $name
+     * @param mixed $type
+     * @param array $options
+     * @return void
+     */
+    public function create($name, $type = null, array $options = array())
+    {
+        return $this->formBuilder->create($name, $type, $options);
+    }
 }

+ 7 - 5
Resources/views/CRUD/base_inline_edit_field.html.twig

@@ -12,12 +12,14 @@ file that was distributed with this source code.
 <div id="sonata-ba-field-container-{{ field_element.vars.id }}" class="sonata-ba-field sonata-ba-field-{{ edit }}-{{ inline }} {% if field_element.vars.errors|length %}sonata-ba-field-error{% endif %}">
 
     {% block label %}
-        {% if field_description.options.name is defined %}
-            {{ form_label(field_element, field_description.options.name) }}
-        {% else %}
-            {{ form_label(field_element) }}
+        {% if inline == 'natural' %}
+            {% if field_description.options.name is defined %}
+                {{ form_label(field_element, field_description.options.name) }}
+            {% else %}
+                {{ form_label(field_element) }}
+            {% endif %}
+            <br />
         {% endif %}
-        <br />
     {% endblock %}
 
     {% block field %}{{ form_widget(field_element) }}{% endblock %}

+ 3 - 3
Route/AdminPoolLoader.php

@@ -13,7 +13,7 @@ namespace Sonata\AdminBundle\Route;
 
 use Symfony\Component\Routing\RouteCollection;
 use Symfony\Component\Routing\Route;
-    
+
 use Symfony\Component\Config\Loader\FileLoader;
 use Symfony\Component\Config\Resource\FileResource;
 
@@ -46,7 +46,7 @@ class AdminPoolLoader extends FileLoader
      * @param null $type
      * @return bool
      */
-    function supports($resource, $type = null)
+    public function supports($resource, $type = null)
     {
         if ($type == 'sonata_admin') {
             return true;
@@ -60,7 +60,7 @@ class AdminPoolLoader extends FileLoader
      * @param null $type
      * @return \Symfony\Component\Routing\RouteCollection
      */
-    function load($resource, $type = null)
+    public function load($resource, $type = null)
     {
         $collection = new RouteCollection;
         foreach ($this->adminServiceIds as $id) {

+ 2 - 0
SonataAdminBundle.php

@@ -13,6 +13,7 @@ namespace Sonata\AdminBundle;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Sonata\AdminBundle\DependencyInjection\AddDependencyCallsPass;
+use Sonata\AdminBundle\DependencyInjection\AddSecurityCallsPass;
 
 class SonataAdminBundle extends Bundle
 {
@@ -22,5 +23,6 @@ class SonataAdminBundle extends Bundle
         parent::build($container);
 
         $container->addCompilerPass(new AddDependencyCallsPass());
+        $container->addCompilerPass(new AddSecurityCallsPass());
     }
 }