diff --git a/docs/images/roles_matrix.png b/docs/images/roles_matrix.png new file mode 100644 index 000000000..701d07157 Binary files /dev/null and b/docs/images/roles_matrix.png differ diff --git a/docs/index.rst b/docs/index.rst index b5f919278..1f58ab034 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,3 +13,4 @@ User Bundle reference/two_step_validation reference/api reference/user_impersonation + reference/roles_matrix diff --git a/docs/reference/roles_matrix.rst b/docs/reference/roles_matrix.rst new file mode 100644 index 000000000..7eb2e57ec --- /dev/null +++ b/docs/reference/roles_matrix.rst @@ -0,0 +1,57 @@ +Roles Matrix +============ + +The ``Sonata\UserBundle\Form\Type\RolesMatrixType`` was built to show all +roles in a matrix view. + + +.. figure:: ../images/roles_matrix.png + :align: center + :alt: Role matrix + :width: 700px + +Every admin has defined default roles like: + + - EDIT + - LIST + - CREATE + - VIEW + - DELETE + - EXPORT + - ALL + +The roles matrix consists of two parts: + +1. one that shows the matrix with each admin and their permissions. +2. another that shows the custom roles which are configured in + ``security.yml`` and lists them as checkboxes (and shows their + inherited roles). + +.. note:: + + The user can just use roles which he is granted. + +How to exclude an admin +----------------------- + +You can set the ``show_in_roles_matrix`` option to ``false``, like this: + +.. configuration-block:: + + .. code-block:: yaml + + # config/services.yaml + + services: + app.admin.post: + class: App\Admin\PostAdmin + tags: + - name: sonata.admin + manager_type: orm + label: "Post" + show_in_roles_matrix: false + arguments: + - ~ + - App\Entity\Post + - ~ + public: true diff --git a/src/DependencyInjection/Compiler/RolesMatrixCompilerPass.php b/src/DependencyInjection/Compiler/RolesMatrixCompilerPass.php new file mode 100644 index 000000000..dd56a94e2 --- /dev/null +++ b/src/DependencyInjection/Compiler/RolesMatrixCompilerPass.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Christian Gripp + * @author Cengizhan Çalışkan + * @author Silas Joisten + */ +final class RolesMatrixCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + foreach ($container->findTaggedServiceIds('sonata.admin') as $name => $items) { + foreach ($items as $item) { + if (($item['show_in_roles_matrix'] ?? true) === false) { + $container->getDefinition('sonata.user.admin_roles_builder') + ->addMethodCall('addExcludeAdmin', [$name]); + } + } + } + } +} diff --git a/src/Form/Type/RolesMatrixType.php b/src/Form/Type/RolesMatrixType.php new file mode 100644 index 000000000..6dc9fd40a --- /dev/null +++ b/src/Form/Type/RolesMatrixType.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Form\Type; + +use Sonata\UserBundle\Security\RolesBuilder\ExpandableRolesBuilderInterface; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Silas Joisten + */ +final class RolesMatrixType extends AbstractType +{ + /** + * @var ExpandableRolesBuilderInterface + */ + private $rolesBuilder; + + public function __construct(ExpandableRolesBuilderInterface $rolesBuilder) + { + $this->rolesBuilder = $rolesBuilder; + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'expanded' => true, + 'choices' => function (Options $options, $parentChoices): array { + if (!empty($parentChoices)) { + return []; + } + + $roles = $this->rolesBuilder->getRoles( + $options['choice_translation_domain'], + $options['expanded'] + ); + $roles = array_keys($roles); + + return array_combine($roles, $roles); + }, + 'choice_translation_domain' => function (Options $options, $value): ?string { + // if choice_translation_domain is true, then it's the same as translation_domain + if (true === $value) { + $value = $options['translation_domain']; + } + if (null === $value) { + // no translation domain yet, try to ask sonata admin + $admin = null; + if (isset($options['sonata_admin'])) { + $admin = $options['sonata_admin']; + } + if (null === $admin && isset($options['sonata_field_description'])) { + $admin = $options['sonata_field_description']->getAdmin(); + } + if (null !== $admin) { + $value = $admin->getTranslationDomain(); + } + } + + return $value; + }, + + 'data_class' => null, + ]); + + // Symfony 2.8 BC + if (method_exists(FormTypeInterface::class, 'setDefaultOptions')) { + $resolver->setDefault('choices_as_values', true); + } + } + + public function getParent(): string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'sonata_roles_matrix'; + } + + public function getName(): string + { + return $this->getBlockPrefix(); + } +} diff --git a/src/Resources/config/admin.xml b/src/Resources/config/admin.xml index cfd1e8d7e..3b943032c 100644 --- a/src/Resources/config/admin.xml +++ b/src/Resources/config/admin.xml @@ -1,7 +1,7 @@ - + @@ -10,9 +10,29 @@ - + + + + + + + + + + + + + + + + %security.role_hierarchy.roles% + + + + + diff --git a/src/Resources/config/twig.xml b/src/Resources/config/twig.xml index a787a00dc..3715bba5d 100644 --- a/src/Resources/config/twig.xml +++ b/src/Resources/config/twig.xml @@ -1,8 +1,12 @@ - + + + + + diff --git a/src/Resources/views/Form/form_admin_fields.html.twig b/src/Resources/views/Form/form_admin_fields.html.twig index a0ad8d91c..de1738dc8 100644 --- a/src/Resources/views/Form/form_admin_fields.html.twig +++ b/src/Resources/views/Form/form_admin_fields.html.twig @@ -27,3 +27,12 @@ file that was distributed with this source code. {% endif %} {% endspaceless %} {% endblock sonata_security_roles_widget %} + +{% block sonata_roles_matrix_widget %} +{% spaceless %} + {{ renderMatrix(form)|raw }} +
    + {{ renderRolesList(form)|raw }} +
+{% endspaceless %} +{% endblock sonata_roles_matrix_widget %} diff --git a/src/Resources/views/Form/roles_matrix.html.twig b/src/Resources/views/Form/roles_matrix.html.twig new file mode 100644 index 000000000..7dcfd3cde --- /dev/null +++ b/src/Resources/views/Form/roles_matrix.html.twig @@ -0,0 +1,41 @@ +{# + +This file is part of the Sonata package. + +(c) Thomas Rabaix + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. + +#} + + + + + {% for label in permission_labels|sort %} + + {% endfor %} + + + + {% for admin_label, roles in grouped_roles %} + + + {% for role, attributes in roles|sort %} + + {% endfor %} + + {% endfor %} + +
{{ label }}
{{ admin_label }} + {{ form_widget(attributes.form, { label: false }) }} + {% if not attributes.is_granted %} + + {% endif %} +
+ diff --git a/src/Resources/views/Form/roles_matrix_list.html.twig b/src/Resources/views/Form/roles_matrix_list.html.twig new file mode 100644 index 000000000..f8ddd8f82 --- /dev/null +++ b/src/Resources/views/Form/roles_matrix_list.html.twig @@ -0,0 +1,21 @@ +{# + +This file is part of the Sonata package. + +(c) Thomas Rabaix + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. + +#} +{% for role, attributes in roles|sort %} +
  • {{ form_widget(attributes.form, {label: attributes.role_translated, value: attributes.role}) }}
  • + {% if not attributes.is_granted %} + + {% endif %} +{% endfor %} diff --git a/src/Security/RolesBuilder/AdminRolesBuilder.php b/src/Security/RolesBuilder/AdminRolesBuilder.php new file mode 100644 index 000000000..b0bb8fee9 --- /dev/null +++ b/src/Security/RolesBuilder/AdminRolesBuilder.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Admin\Pool; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * @author Silas Joisten + */ +final class AdminRolesBuilder implements AdminRolesBuilderInterface +{ + /** + * @var AuthorizationCheckerInterface + */ + private $authorizationChecker; + + /** + * @var Pool + */ + private $pool; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string [] + */ + private $excludeAdmins = []; + + public function __construct( + AuthorizationCheckerInterface $authorizationChecker, + Pool $pool, + TranslatorInterface $translator + ) { + $this->authorizationChecker = $authorizationChecker; + $this->pool = $pool; + $this->translator = $translator; + } + + public function getPermissionLabels(): array + { + $permissionLabels = []; + foreach ($this->getRoles() as $attributes) { + if (isset($attributes['label'])) { + $permissionLabels[$attributes['label']] = $attributes['label']; + } + } + + return $permissionLabels; + } + + public function getExcludeAdmins(): array + { + return $this->excludeAdmins; + } + + public function addExcludeAdmin(string $exclude): void + { + $this->excludeAdmins[] = $exclude; + } + + public function getRoles(string $domain = null): array + { + $adminRoles = []; + foreach ($this->pool->getAdminServiceIds() as $id) { + if (in_array($id, $this->excludeAdmins)) { + continue; + } + + $admin = $this->pool->getInstance($id); + $securityHandler = $admin->getSecurityHandler(); + $baseRole = $securityHandler->getBaseRole($admin); + foreach (array_keys($admin->getSecurityInformation()) as $key) { + $role = sprintf($baseRole, $key); + $adminRoles[$role] = [ + 'role' => $role, + 'label' => $key, + 'role_translated' => $this->translateRole($role, $domain), + 'is_granted' => $this->isMaster($admin) || $this->authorizationChecker->isGranted($role), + 'admin_label' => $admin->getTranslator()->trans($admin->getLabel()), + ]; + } + } + + return $adminRoles; + } + + private function isMaster(AdminInterface $admin): bool + { + return $admin->isGranted('MASTER') || $admin->isGranted('OPERATOR') + || $this->authorizationChecker->isGranted($this->pool->getOption('role_super_admin')); + } + + private function translateRole(string $role, $domain): string + { + if ($domain) { + return $this->translator->trans($role, [], $domain); + } + + return $role; + } +} diff --git a/src/Security/RolesBuilder/AdminRolesBuilderInterface.php b/src/Security/RolesBuilder/AdminRolesBuilderInterface.php new file mode 100644 index 000000000..ab2914fa2 --- /dev/null +++ b/src/Security/RolesBuilder/AdminRolesBuilderInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +/** + * @author Silas Joisten + */ +interface AdminRolesBuilderInterface extends RolesBuilderInterface, PermissionLabelsBuilderInterface +{ +} diff --git a/src/Security/RolesBuilder/ExpandableRolesBuilderInterface.php b/src/Security/RolesBuilder/ExpandableRolesBuilderInterface.php new file mode 100644 index 000000000..43979d60a --- /dev/null +++ b/src/Security/RolesBuilder/ExpandableRolesBuilderInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +/** + * @author Silas Joisten + */ +interface ExpandableRolesBuilderInterface extends RolesBuilderInterface +{ + public function getExpandedRoles(?string $domain = null): array; +} diff --git a/src/Security/RolesBuilder/MatrixRolesBuilder.php b/src/Security/RolesBuilder/MatrixRolesBuilder.php new file mode 100644 index 000000000..c9b300bf9 --- /dev/null +++ b/src/Security/RolesBuilder/MatrixRolesBuilder.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +/** + * @author Silas Joisten + */ +final class MatrixRolesBuilder implements MatrixRolesBuilderInterface +{ + /** + * @var TokenStorageInterface + */ + private $tokenStorage; + + /** + * @var AdminRolesBuilderInterface + */ + private $adminRolesBuilder; + + /** + * @var ExpandableRolesBuilderInterface + */ + private $securityRolesBuilder; + + public function __construct( + TokenStorageInterface $tokenStorage, + AdminRolesBuilderInterface $adminRolesBuilder, + ExpandableRolesBuilderInterface $securityRolesBuilder + ) { + $this->tokenStorage = $tokenStorage; + $this->adminRolesBuilder = $adminRolesBuilder; + $this->securityRolesBuilder = $securityRolesBuilder; + } + + public function getRoles(?string $domain = null): array + { + if (!$this->tokenStorage->getToken()) { + return []; + } + + return array_merge( + $this->securityRolesBuilder->getRoles($domain), + $this->adminRolesBuilder->getRoles($domain) + ); + } + + public function getExpandedRoles(?string $domain = null): array + { + if (!$this->tokenStorage->getToken()) { + return []; + } + + return array_merge( + $this->securityRolesBuilder->getExpandedRoles($domain), + $this->adminRolesBuilder->getRoles($domain) + ); + } + + public function getPermissionLabels(): array + { + return $this->adminRolesBuilder->getPermissionLabels(); + } +} diff --git a/src/Security/RolesBuilder/MatrixRolesBuilderInterface.php b/src/Security/RolesBuilder/MatrixRolesBuilderInterface.php new file mode 100644 index 000000000..1734138b9 --- /dev/null +++ b/src/Security/RolesBuilder/MatrixRolesBuilderInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +/** + * @author Silas Joisten + */ +interface MatrixRolesBuilderInterface extends ExpandableRolesBuilderInterface, PermissionLabelsBuilderInterface +{ +} diff --git a/src/Security/RolesBuilder/PermissionLabelsBuilderInterface.php b/src/Security/RolesBuilder/PermissionLabelsBuilderInterface.php new file mode 100644 index 000000000..88f50055d --- /dev/null +++ b/src/Security/RolesBuilder/PermissionLabelsBuilderInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +/** + * @author Silas Joisten + */ +interface PermissionLabelsBuilderInterface +{ + /** + * @return string[] + */ + public function getPermissionLabels(): array; +} diff --git a/src/Security/RolesBuilder/RolesBuilderInterface.php b/src/Security/RolesBuilder/RolesBuilderInterface.php new file mode 100644 index 000000000..9a71d8fb5 --- /dev/null +++ b/src/Security/RolesBuilder/RolesBuilderInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +/** + * @author Silas Joisten + */ +interface RolesBuilderInterface +{ + public function getRoles(?string $domain = null): array; +} diff --git a/src/Security/RolesBuilder/SecurityRolesBuilder.php b/src/Security/RolesBuilder/SecurityRolesBuilder.php new file mode 100644 index 000000000..52f9445dd --- /dev/null +++ b/src/Security/RolesBuilder/SecurityRolesBuilder.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Security\RolesBuilder; + +use Sonata\AdminBundle\Admin\Pool; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * @author Silas Joisten + */ +final class SecurityRolesBuilder implements ExpandableRolesBuilderInterface +{ + /** + * @var AuthorizationCheckerInterface + */ + private $authorizationChecker; + + /** + * @var Pool + */ + private $pool; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * @var string [] + */ + private $rolesHierarchy; + + public function __construct( + AuthorizationCheckerInterface $authorizationChecker, + Pool $pool, + TranslatorInterface $translator, + array $rolesHierarchy = [] + ) { + $this->authorizationChecker = $authorizationChecker; + $this->pool = $pool; + $this->translator = $translator; + $this->rolesHierarchy = $rolesHierarchy; + } + + public function getExpandedRoles(?string $domain = null): array + { + $securityRoles = []; + foreach ($hierarchy = $this->getHierarchy() as $role => $childRoles) { + $translatedRoles = array_map( + [$this, 'translateRole'], + $childRoles, + array_fill(0, count($childRoles), $domain) + ); + + $translatedRoles = count($translatedRoles) > 0 ? ': '.implode(', ', $translatedRoles) : ''; + $securityRoles[$role] = [ + 'role' => $role, + 'role_translated' => $role.$translatedRoles, + 'is_granted' => $this->authorizationChecker->isGranted($role), + ]; + + $securityRoles = array_merge( + $securityRoles, + $this->getSecurityRoles($hierarchy, $childRoles, $domain) + ); + } + + return $securityRoles; + } + + public function getRoles(?string $domain = null): array + { + $securityRoles = []; + foreach ($hierarchy = $this->getHierarchy() as $role => $childRoles) { + $securityRoles[$role] = $this->getSecurityRole($role, $domain); + $securityRoles = array_merge( + $securityRoles, + $this->getSecurityRoles($hierarchy, $childRoles, $domain) + ); + } + + return $securityRoles; + } + + private function getHierarchy(): array + { + return array_merge([ + $this->pool->getOption('role_super_admin') => [], + $this->pool->getOption('role_admin') => [], + ], $this->rolesHierarchy); + } + + private function getSecurityRole(string $role, ?string $domain): array + { + return [ + 'role' => $role, + 'role_translated' => $this->translateRole($role, $domain), + 'is_granted' => $this->authorizationChecker->isGranted($role), + ]; + } + + private function getSecurityRoles(array $hierarchy, array $roles, ?string $domain): array + { + $securityRoles = []; + foreach ($roles as $role) { + if (!array_key_exists($role, $hierarchy) && !isset($securityRoles[$role]) + && !$this->recursiveArraySearch($role, $securityRoles)) { + $securityRoles[$role] = $this->getSecurityRole($role, $domain); + } + } + + return $securityRoles; + } + + private function translateRole(string $role, $domain): string + { + if ($domain) { + return $this->translator->trans($role, [], $domain); + } + + return $role; + } + + private function recursiveArraySearch(string $role, array $roles): bool + { + foreach ($roles as $key => $value) { + if ($role === $key || (is_array($value) && true === $this->recursiveArraySearch($role, $value))) { + return true; + } + } + + return false; + } +} diff --git a/src/SonataUserBundle.php b/src/SonataUserBundle.php index a5567ea2c..63494c92b 100644 --- a/src/SonataUserBundle.php +++ b/src/SonataUserBundle.php @@ -15,6 +15,7 @@ use Sonata\CoreBundle\Form\FormHelper; use Sonata\UserBundle\DependencyInjection\Compiler\GlobalVariablesCompilerPass; +use Sonata\UserBundle\DependencyInjection\Compiler\RolesMatrixCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -26,6 +27,7 @@ class SonataUserBundle extends Bundle public function build(ContainerBuilder $container): void { $container->addCompilerPass(new GlobalVariablesCompilerPass()); + $container->addCompilerPass(new RolesMatrixCompilerPass()); $this->registerFormMapping(); } diff --git a/src/Twig/RolesMatrixExtension.php b/src/Twig/RolesMatrixExtension.php new file mode 100644 index 000000000..444624298 --- /dev/null +++ b/src/Twig/RolesMatrixExtension.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Twig; + +use Sonata\UserBundle\Security\RolesBuilder\MatrixRolesBuilderInterface; +use Symfony\Component\Form\FormView; +use Twig\Environment; +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +/** + * @author Christian Gripp + * @author Cengizhan Çalışkan + * @author Silas Joisten + */ +final class RolesMatrixExtension extends AbstractExtension +{ + /** + * @var MatrixRolesBuilderInterface + */ + private $rolesBuilder; + + public function __construct(MatrixRolesBuilderInterface $rolesBuilder) + { + $this->rolesBuilder = $rolesBuilder; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('renderMatrix', [$this, 'renderMatrix'], ['needs_environment' => true]), + new TwigFunction( + 'renderRolesList', + [$this, 'renderRolesList'], + ['needs_environment' => true] + ), + ]; + } + + public function getName(): string + { + return self::class; + } + + public function renderRolesList(Environment $environment, FormView $form): string + { + $roles = $this->rolesBuilder->getRoles(); + foreach ($roles as $role => $attributes) { + if (isset($attributes['admin_label'])) { + unset($roles[$role]); + continue; + } + + $roles[$role] = $attributes; + foreach ($form->getIterator() as $child) { + if ($child->vars['value'] == $role) { + $roles[$role]['form'] = $child; + } + } + } + + return $environment->render('@SonataUser/Form/roles_matrix_list.html.twig', [ + 'roles' => $roles, + ]); + } + + public function renderMatrix(Environment $environment, FormView $form): string + { + $groupedRoles = []; + foreach ($this->rolesBuilder->getRoles() as $role => $attributes) { + if (!isset($attributes['admin_label'])) { + continue; + } + + $groupedRoles[$attributes['admin_label']][$role] = $attributes; + foreach ($form->getIterator() as $child) { + if ($child->vars['value'] == $role) { + $groupedRoles[$attributes['admin_label']][$role]['form'] = $child; + } + } + } + + return $environment->render('@SonataUser/Form/roles_matrix.html.twig', [ + 'grouped_roles' => $groupedRoles, + 'permission_labels' => $this->rolesBuilder->getPermissionLabels(), + ]); + } +} diff --git a/tests/DependencyInjection/Compiler/RolesMatrixCompilerPassTest.php b/tests/DependencyInjection/Compiler/RolesMatrixCompilerPassTest.php new file mode 100644 index 000000000..25d83e69a --- /dev/null +++ b/tests/DependencyInjection/Compiler/RolesMatrixCompilerPassTest.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Sonata\UserBundle\DependencyInjection\Compiler\RolesMatrixCompilerPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; + +/** + * @author Silas Joisten + */ +final class RolesMatrixCompilerPassTest extends TestCase +{ + /** + * @test + */ + public function process(): void + { + $definition = $this->createMock(Definition::class); + + $container = $this->createMock(ContainerBuilder::class); + $container + ->expects($this->once()) + ->method('getDefinition') + ->with('sonata.user.admin_roles_builder') + ->willReturn($definition); + + $taggedServices = [ + 'sonata.admin.foo' => [0 => ['show_in_roles_matrix' => true]], + 'sonata.admin.bar' => [0 => ['show_in_roles_matrix' => false]], + 'sonata.admin.test' => [], + ]; + + $container + ->expects($this->once()) + ->method('findTaggedServiceIds') + ->with('sonata.admin') + ->willReturn($taggedServices); + + $definition + ->expects($this->once()) + ->method('addMethodCall') + ->with('addExcludeAdmin', ['sonata.admin.bar']); + + $compilerPass = new RolesMatrixCompilerPass(); + $compilerPass->process($container); + } +} diff --git a/tests/Form/Type/RolesMatrixTypeTest.php b/tests/Form/Type/RolesMatrixTypeTest.php new file mode 100755 index 000000000..75bc2a969 --- /dev/null +++ b/tests/Form/Type/RolesMatrixTypeTest.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Tests\Form\Type; + +use Sonata\UserBundle\Form\Type\RolesMatrixType; +use Sonata\UserBundle\Security\RolesBuilder\ExpandableRolesBuilderInterface; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\Form\PreloadedExtension; +use Symfony\Component\Form\Test\TypeTestCase; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Silas Joisten + */ +final class RolesMatrixTypeTest extends TypeTestCase +{ + private $roleBuilder; + + public function testGetDefaultOptions(): void + { + $type = new RolesMatrixType($this->roleBuilder); + + $optionResolver = new OptionsResolver(); + $type->configureOptions($optionResolver); + + $options = $optionResolver->resolve(); + $this->assertCount(3, $options['choices']); + + if (method_exists(FormTypeInterface::class, 'setDefaultOptions')) { + $this->assertTrue($options['choices_as_values']); + } + } + + public function testGetParent(): void + { + $type = new RolesMatrixType($this->roleBuilder); + + $this->assertEquals(ChoiceType::class, $type->getParent()); + } + + public function testSubmitValidData(): void + { + $form = $this->factory->create(RolesMatrixType::class, null, [ + 'multiple' => true, + 'expanded' => true, + 'required' => false, + ]); + + $form->submit([0 => 'ROLE_FOO']); + + $this->assertTrue($form->isSynchronized()); + $this->assertCount(1, $form->getData()); + $this->assertTrue(in_array('ROLE_FOO', $form->getData())); + } + + public function testSubmitInvalidData(): void + { + $form = $this->factory->create(RolesMatrixType::class, null, [ + 'multiple' => true, + 'expanded' => true, + 'required' => false, + ]); + + $form->submit([0 => 'ROLE_NOT_EXISTS']); + + $this->assertFalse($form->isSynchronized()); + $this->assertNull($form->getData()); + } + + public function testChoicesAsValues(): void + { + $resolver = new OptionsResolver(); + $type = new RolesMatrixType($this->roleBuilder); + + // If 'choices_as_values' option is not defined (Symfony >= 3.0), default value should not be set. + $type->configureOptions($resolver); + + // If 'choices_as_values' option is defined (Symfony 2.8), default value should be set to true. + $resolver->setDefault('choices_as_values', true); + $type->configureOptions($resolver); + $options = $resolver->resolve(); + + $this->assertTrue($resolver->hasDefault('choices_as_values')); + $this->assertTrue($options['choices_as_values']); + } + + protected function getExtensions() + { + $this->roleBuilder = $this->createMock(ExpandableRolesBuilderInterface::class); + + $this->roleBuilder->expects($this->any())->method('getRoles')->will($this->returnValue([ + 'ROLE_FOO' => 'ROLE_FOO', + 'ROLE_USER' => 'ROLE_USER', + 'ROLE_ADMIN' => 'ROLE_ADMIN: ROLE_USER', + ])); + + $childType = new RolesMatrixType($this->roleBuilder); + + return [new PreloadedExtension([ + $childType->getName() => $childType, + ], [])]; + } +} diff --git a/tests/Security/RolesBuilder/AdminRolesBuilderTest.php b/tests/Security/RolesBuilder/AdminRolesBuilderTest.php new file mode 100644 index 000000000..75ff10d90 --- /dev/null +++ b/tests/Security/RolesBuilder/AdminRolesBuilderTest.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Tests\Security\RolesBuilder; + +use PHPUnit\Framework\TestCase; +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Admin\Pool; +use Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface; +use Sonata\UserBundle\Security\RolesBuilder\AdminRolesBuilder; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * @author Silas Joisten + */ +final class AdminRolesBuilderTest extends TestCase +{ + private $securityHandler; + private $authorizationChecker; + private $admin; + private $tokenStorage; + private $token; + private $pool; + private $translator; + + private $securityInformation = [ + 'GUEST' => [0 => 'VIEW', 1 => 'LIST'], + 'STAFF' => [0 => 'EDIT', 1 => 'LIST', 2 => 'CREATE'], + 'EDITOR' => [0 => 'OPERATOR', 1 => 'EXPORT'], + 'ADMIN' => [0 => 'MASTER'], + ]; + + public function setUp(): void + { + $this->securityHandler = $this->createMock(SecurityHandlerInterface::class); + $this->authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); + $this->admin = $this->createMock(AdminInterface::class); + $this->tokenStorage = $this->createMock(TokenStorageInterface::class); + $this->token = $this->createMock(TokenInterface::class); + $this->pool = $this->createMock(Pool::class); + $this->translator = $this->createMock(TranslatorInterface::class); + } + + public function testGetPermissionLabels(): void + { + $this->translator->method('trans'); + + $this->securityHandler->method('getBaseRole') + ->willReturn('ROLE_SONATA_FOO_%s'); + + $this->admin->method('getSecurityHandler') + ->willReturn($this->securityHandler); + + $this->admin->method('getTranslator') + ->willReturn($this->translator); + + $this->admin->method('getSecurityInformation') + ->willReturn($this->securityInformation); + + $this->pool->expects($this->once()) + ->method('getAdminServiceIds') + ->willReturn(['sonata.admin.bar']); + + $this->pool->expects($this->once()) + ->method('getInstance') + ->with('sonata.admin.bar') + ->willReturn($this->admin); + + $rolesBuilder = new AdminRolesBuilder( + $this->authorizationChecker, + $this->pool, + $this->translator + ); + + $expected = [ + 'GUEST' => 'GUEST', + 'STAFF' => 'STAFF', + 'EDITOR' => 'EDITOR', + 'ADMIN' => 'ADMIN', + ]; + + $this->assertSame($expected, $rolesBuilder->getPermissionLabels()); + } + + public function testGetRoles(): void + { + $this->translator->method('trans') + ->willReturn('Foo'); + + $this->securityHandler->method('getBaseRole') + ->willReturn('ROLE_SONATA_FOO_%s'); + + $this->admin->method('getSecurityHandler') + ->willReturn($this->securityHandler); + + $this->admin->method('getTranslator') + ->willReturn($this->translator); + + $this->admin->method('getSecurityInformation') + ->willReturn($this->securityInformation); + + $this->admin->method('getLabel') + ->willReturn('Foo'); + + $this->pool->expects($this->once()) + ->method('getAdminServiceIds') + ->willReturn(['sonata.admin.bar']); + + $this->pool->expects($this->once()) + ->method('getInstance') + ->with('sonata.admin.bar') + ->willReturn($this->admin); + + $rolesBuilder = new AdminRolesBuilder( + $this->authorizationChecker, + $this->pool, + $this->translator + ); + + $expected = [ + 'ROLE_SONATA_FOO_GUEST' => [ + 'role' => 'ROLE_SONATA_FOO_GUEST', + 'label' => 'GUEST', + 'role_translated' => 'ROLE_SONATA_FOO_GUEST', + 'is_granted' => false, + 'admin_label' => 'Foo', + ], + 'ROLE_SONATA_FOO_STAFF' => [ + 'role' => 'ROLE_SONATA_FOO_STAFF', + 'label' => 'STAFF', + 'role_translated' => 'ROLE_SONATA_FOO_STAFF', + 'is_granted' => false, + 'admin_label' => 'Foo', + ], + 'ROLE_SONATA_FOO_EDITOR' => [ + 'role' => 'ROLE_SONATA_FOO_EDITOR', + 'label' => 'EDITOR', + 'role_translated' => 'ROLE_SONATA_FOO_EDITOR', + 'is_granted' => false, + 'admin_label' => 'Foo', + ], + 'ROLE_SONATA_FOO_ADMIN' => [ + 'role' => 'ROLE_SONATA_FOO_ADMIN', + 'label' => 'ADMIN', + 'role_translated' => 'ROLE_SONATA_FOO_ADMIN', + 'is_granted' => false, + 'admin_label' => 'Foo', + ], + ]; + + $this->assertSame($expected, $rolesBuilder->getRoles()); + } + + public function testGetAddExcludeAdmins(): void + { + $rolesBuilder = new AdminRolesBuilder( + $this->authorizationChecker, + $this->pool, + $this->translator + ); + $rolesBuilder->addExcludeAdmin('sonata.admin.bar'); + + $this->assertSame(['sonata.admin.bar'], $rolesBuilder->getExcludeAdmins()); + } +} diff --git a/tests/Security/RolesBuilder/MatrixRolesBuilderTest.php b/tests/Security/RolesBuilder/MatrixRolesBuilderTest.php new file mode 100644 index 000000000..393cca762 --- /dev/null +++ b/tests/Security/RolesBuilder/MatrixRolesBuilderTest.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Tests\Security\RolesBuilder; + +use PHPUnit\Framework\TestCase; +use Sonata\UserBundle\Security\RolesBuilder\AdminRolesBuilderInterface; +use Sonata\UserBundle\Security\RolesBuilder\ExpandableRolesBuilderInterface; +use Sonata\UserBundle\Security\RolesBuilder\MatrixRolesBuilder; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * @author Silas Joisten + */ +final class MatrixRolesBuilderTest extends TestCase +{ + private $tokenStorage; + private $token; + private $adminRolesBuilder; + private $securityRolesBuilder; + + public function setUp(): void + { + $this->tokenStorage = $this->createMock(TokenStorageInterface::class); + $this->token = $this->createMock(TokenInterface::class); + $this->adminRolesBuilder = $this->createMock(AdminRolesBuilderInterface::class); + $this->securityRolesBuilder = $this->createMock(ExpandableRolesBuilderInterface::class); + } + + public function testGetPermissionLabels(): void + { + $expected = ['EDIT' => 'EDIT', 'LIST' => 'LIST', 'CREATE' => 'CREATE']; + + $this->adminRolesBuilder->method('getPermissionLabels') + ->willReturn($expected); + + $matrixRolesBuilder = new MatrixRolesBuilder( + $this->tokenStorage, + $this->adminRolesBuilder, + $this->securityRolesBuilder + ); + + $this->assertSame($expected, $matrixRolesBuilder->getPermissionLabels()); + } + + public function testGetRoles(): void + { + $this->tokenStorage->method('getToken') + ->willReturn($this->token); + + $adminRoles = [ + 'ROLE_SONATA_FOO_GUEST' => [ + 'role' => 'ROLE_SONATA_FOO_GUEST', + 'label' => 'GUEST', + 'role_translated' => 'ROLE_SONATA_FOO_GUEST', + 'is_granted' => false, + 'admin_label' => 'Foo', + ], + ]; + + $this->adminRolesBuilder->method('getRoles') + ->willReturn($adminRoles); + + $securityRoles = [ + 'ROLE_FOO' => [ + 'role' => 'ROLE_FOO', + 'role_translated' => 'ROLE_FOO: ROLE_BAR, ROLE_ADMIN', + 'is_granted' => true, + ], + ]; + + $this->securityRolesBuilder->method('getRoles') + ->willReturn($securityRoles); + + $matrixRolesBuilder = new MatrixRolesBuilder( + $this->tokenStorage, + $this->adminRolesBuilder, + $this->securityRolesBuilder + ); + + $expected = array_merge($securityRoles, $adminRoles); + + $this->assertSame($expected, $matrixRolesBuilder->getRoles()); + } + + public function testGetRolesNoToken(): void + { + $this->tokenStorage->method('getToken') + ->willReturn(null); + + $matrixRolesBuilder = new MatrixRolesBuilder( + $this->tokenStorage, + $this->adminRolesBuilder, + $this->securityRolesBuilder + ); + + $this->assertEmpty($matrixRolesBuilder->getRoles()); + } +} diff --git a/tests/Security/RolesBuilder/SecurityRolesBuilderTest.php b/tests/Security/RolesBuilder/SecurityRolesBuilderTest.php new file mode 100644 index 000000000..6c677fd8c --- /dev/null +++ b/tests/Security/RolesBuilder/SecurityRolesBuilderTest.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Tests\Security\RolesBuilder; + +use PHPUnit\Framework\TestCase; +use Sonata\AdminBundle\Admin\AdminInterface; +use Sonata\AdminBundle\Admin\Pool; +use Sonata\UserBundle\Security\RolesBuilder\SecurityRolesBuilder; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * @author Silas Joisten + */ +final class SecurityRolesBuilderTest extends TestCase +{ + private $authorizationChecker; + private $admin; + private $pool; + private $translator; + private $rolesHierarchy = ['ROLE_FOO' => ['ROLE_BAR', 'ROLE_ADMIN']]; + + public function setUp(): void + { + $this->authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); + $this->admin = $this->createMock(AdminInterface::class); + $this->pool = $this->createMock(Pool::class); + $this->translator = $this->createMock(TranslatorInterface::class); + } + + public function testGetRoles(): void + { + $this->pool->expects($this->at(0)) + ->method('getOption') + ->with('role_super_admin') + ->willReturn('ROLE_SUPER_ADMIN'); + + $this->pool->expects($this->at(1)) + ->method('getOption') + ->with('role_admin') + ->willReturn('ROLE_SONATA_ADMIN'); + + $securityRolesBuilder = new SecurityRolesBuilder( + $this->authorizationChecker, + $this->pool, + $this->translator, + $this->rolesHierarchy + ); + + $this->authorizationChecker->method('isGranted') + ->willReturn(true); + + $expected = [ + 'ROLE_SUPER_ADMIN' => [ + 'role' => 'ROLE_SUPER_ADMIN', + 'role_translated' => 'ROLE_SUPER_ADMIN', + 'is_granted' => true, + ], + 'ROLE_SONATA_ADMIN' => [ + 'role' => 'ROLE_SONATA_ADMIN', + 'role_translated' => 'ROLE_SONATA_ADMIN', + 'is_granted' => true, + ], + 'ROLE_FOO' => [ + 'role' => 'ROLE_FOO', + 'role_translated' => 'ROLE_FOO: ROLE_BAR, ROLE_ADMIN', + 'is_granted' => true, + ], + 'ROLE_BAR' => [ + 'role' => 'ROLE_BAR', + 'role_translated' => 'ROLE_BAR', + 'is_granted' => true, + ], + 'ROLE_ADMIN' => [ + 'role' => 'ROLE_ADMIN', + 'role_translated' => 'ROLE_ADMIN', + 'is_granted' => true, + ], + ]; + + $this->assertEquals($expected, $securityRolesBuilder->getExpandedRoles()); + } + + public function testGetRolesNotExpanded(): void + { + $this->pool->expects($this->at(0)) + ->method('getOption') + ->with('role_super_admin') + ->willReturn('ROLE_SUPER_ADMIN'); + + $this->pool->expects($this->at(1)) + ->method('getOption') + ->with('role_admin') + ->willReturn('ROLE_SONATA_ADMIN'); + + $securityRolesBuilder = new SecurityRolesBuilder( + $this->authorizationChecker, + $this->pool, + $this->translator, + $this->rolesHierarchy + ); + + $this->authorizationChecker->method('isGranted') + ->willReturn(true); + + $expected = [ + 'ROLE_SUPER_ADMIN' => [ + 'role' => 'ROLE_SUPER_ADMIN', + 'role_translated' => 'ROLE_SUPER_ADMIN', + 'is_granted' => true, + ], + 'ROLE_SONATA_ADMIN' => [ + 'role' => 'ROLE_SONATA_ADMIN', + 'role_translated' => 'ROLE_SONATA_ADMIN', + 'is_granted' => true, + ], + 'ROLE_FOO' => [ + 'role' => 'ROLE_FOO', + 'role_translated' => 'ROLE_FOO', + 'is_granted' => true, + ], + 'ROLE_BAR' => [ + 'role' => 'ROLE_BAR', + 'role_translated' => 'ROLE_BAR', + 'is_granted' => true, + ], + 'ROLE_ADMIN' => [ + 'role' => 'ROLE_ADMIN', + 'role_translated' => 'ROLE_ADMIN', + 'is_granted' => true, + ], + ]; + + $this->assertEquals($expected, $securityRolesBuilder->getRoles(null, false)); + } + + public function testGetRolesWithExistingRole(): void + { + $this->pool->expects($this->at(0)) + ->method('getOption') + ->with('role_super_admin') + ->willReturn('ROLE_SUPER_ADMIN'); + + $this->pool->expects($this->at(1)) + ->method('getOption') + ->with('role_admin') + ->willReturn('ROLE_SONATA_ADMIN'); + + $this->rolesHierarchy['ROLE_STAFF'] = ['ROLE_SUPER_ADMIN', 'ROLE_SUPER_ADMIN']; + + $securityRolesBuilder = new SecurityRolesBuilder( + $this->authorizationChecker, + $this->pool, + $this->translator, + $this->rolesHierarchy + ); + + $this->authorizationChecker->method('isGranted') + ->willReturn(true); + + $expected = [ + 'ROLE_SUPER_ADMIN' => [ + 'role' => 'ROLE_SUPER_ADMIN', + 'role_translated' => 'ROLE_SUPER_ADMIN', + 'is_granted' => true, + ], + 'ROLE_SONATA_ADMIN' => [ + 'role' => 'ROLE_SONATA_ADMIN', + 'role_translated' => 'ROLE_SONATA_ADMIN', + 'is_granted' => true, + ], + 'ROLE_FOO' => [ + 'role' => 'ROLE_FOO', + 'role_translated' => 'ROLE_FOO: ROLE_BAR, ROLE_ADMIN', + 'is_granted' => true, + ], + 'ROLE_BAR' => [ + 'role' => 'ROLE_BAR', + 'role_translated' => 'ROLE_BAR', + 'is_granted' => true, + ], + 'ROLE_ADMIN' => [ + 'role' => 'ROLE_ADMIN', + 'role_translated' => 'ROLE_ADMIN', + 'is_granted' => true, + ], + 'ROLE_STAFF' => [ + 'role' => 'ROLE_STAFF', + 'role_translated' => 'ROLE_STAFF: ROLE_SUPER_ADMIN, ROLE_SUPER_ADMIN', + 'is_granted' => true, + ], + ]; + + $this->assertEquals($expected, $securityRolesBuilder->getExpandedRoles()); + } +} diff --git a/tests/Twig/RolesMatrixExtensionTest.php b/tests/Twig/RolesMatrixExtensionTest.php new file mode 100644 index 000000000..290c5fbca --- /dev/null +++ b/tests/Twig/RolesMatrixExtensionTest.php @@ -0,0 +1,315 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\UserBundle\Tests\Twig; + +use PHPUnit\Framework\TestCase; +use Sonata\UserBundle\Security\RolesBuilder\MatrixRolesBuilderInterface; +use Sonata\UserBundle\Twig\RolesMatrixExtension; +use Symfony\Component\Form\FormView; +use Twig\Environment; + +/** + * @author Silas Joisten + */ +final class RolesMatrixExtensionTest extends TestCase +{ + private $rolesBuilder; + private $environment; + private $formView; + + /** + * {@inheritdoc} + */ + public function setUp(): void + { + $this->rolesBuilder = $this->createMock(MatrixRolesBuilderInterface::class); + $this->environment = $this->createMock(Environment::class); + $this->formView = $this->createMock(FormView::class); + } + + public function testGetName(): void + { + $rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder); + $this->assertSame(RolesMatrixExtension::class, $rolesMatrixExtension->getName()); + } + + /** + * @test + */ + public function renderRolesListWithAdminLabel(): void + { + $roles = [ + 'SUPER_TEST_ROLE' => [ + 'role' => 'SUPER_TEST_ROLE', + 'role_translated' => 'SUPER TEST ROLE TRANSLATED', + 'is_granted' => true, + 'admin_label' => 'admin_name', + ], + ]; + $this->rolesBuilder + ->expects($this->once()) + ->method('getRoles') + ->willReturn($roles); + + $this->formView + ->expects($this->never()) + ->method('getIterator'); + + $this->environment + ->expects($this->once()) + ->method('render') + ->with('@SonataUser/Form/roles_matrix_list.html.twig', ['roles' => []]) + ->willReturn('') + ; + + $rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder); + $rolesMatrixExtension->renderRolesList($this->environment, $this->formView); + } + + /** + * @test + */ + public function renderRolesList(): void + { + $roles = [ + 'SUPER_TEST_ROLE' => [ + 'role' => 'SUPER_TEST_ROLE', + 'role_translated' => 'SUPER TEST ROLE TRANSLATED', + 'is_granted' => true, + ], + ]; + $this->rolesBuilder + ->expects($this->once()) + ->method('getRoles') + ->willReturn($roles); + + $form = new FormView(); + $form->vars['value'] = 'SUPER_TEST_ROLE'; + + $this->formView + ->method('getIterator') + ->willReturn([$form]); + + $this->environment + ->expects($this->once()) + ->method('render') + ->with('@SonataUser/Form/roles_matrix_list.html.twig', [ + 'roles' => [ + 'SUPER_TEST_ROLE' => [ + 'role' => 'SUPER_TEST_ROLE', + 'role_translated' => 'SUPER TEST ROLE TRANSLATED', + 'is_granted' => true, + 'form' => $form, + ], + ], + ]) + ->willReturn(''); + + $rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder); + $rolesMatrixExtension->renderRolesList($this->environment, $this->formView); + } + + /** + * @test + */ + public function renderRolesListWithoutFormValue(): void + { + $roles = [ + 'SUPER_TEST_ROLE' => [ + 'role' => 'SUPER_TEST_ROLE', + 'role_translated' => 'SUPER TEST ROLE TRANSLATED', + 'is_granted' => true, + ], + ]; + $this->rolesBuilder + ->expects($this->once()) + ->method('getRoles') + ->willReturn($roles); + + $form = new FormView(); + $form->vars['value'] = 'WRONG_VALUE'; + + $this->formView + ->method('getIterator') + ->willReturn([$form]); + + $this->environment + ->expects($this->once()) + ->method('render') + ->with('@SonataUser/Form/roles_matrix_list.html.twig', [ + 'roles' => [ + 'SUPER_TEST_ROLE' => [ + 'role' => 'SUPER_TEST_ROLE', + 'role_translated' => 'SUPER TEST ROLE TRANSLATED', + 'is_granted' => true, + ], + ], + ]) + ->willReturn(''); + + $rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder); + $rolesMatrixExtension->renderRolesList($this->environment, $this->formView); + } + + /** + * @test + */ + public function renderMatrixWithoutAdminLabels(): void + { + $roles = [ + 'BASE_ROLE_FOO_%s' => [ + 'role' => 'BASE_ROLE_FOO_EDIT', + 'label' => 'EDIT', + 'role_translated' => 'ROLE FOO TRANSLATED', + 'is_granted' => true, + ], + ]; + $this->rolesBuilder + ->expects($this->once()) + ->method('getRoles') + ->willReturn($roles); + + $this->rolesBuilder + ->expects($this->once()) + ->method('getPermissionLabels') + ->willReturn(['EDIT', 'CREATE']); + + $this->formView + ->expects($this->never()) + ->method('getIterator'); + + $this->environment + ->expects($this->once()) + ->method('render') + ->with('@SonataUser/Form/roles_matrix.html.twig', [ + 'grouped_roles' => [], + 'permission_labels' => ['EDIT', 'CREATE'], + ]) + ->willReturn(''); + + $rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder); + $rolesMatrixExtension->renderMatrix($this->environment, $this->formView); + } + + /** + * @test + */ + public function renderMatrix(): void + { + $roles = [ + 'BASE_ROLE_FOO_EDIT' => [ + 'role' => 'BASE_ROLE_FOO_EDIT', + 'label' => 'EDIT', + 'role_translated' => 'ROLE FOO TRANSLATED', + 'admin_label' => 'fooadmin', + 'is_granted' => true, + ], + ]; + $this->rolesBuilder + ->expects($this->once()) + ->method('getRoles') + ->willReturn($roles); + + $this->rolesBuilder + ->expects($this->once()) + ->method('getPermissionLabels') + ->willReturn(['EDIT', 'CREATE']); + + $form = new FormView(); + $form->vars['value'] = 'BASE_ROLE_FOO_EDIT'; + + $this->formView + ->expects($this->once()) + ->method('getIterator') + ->willReturn([$form]); + + $this->environment + ->expects($this->once()) + ->method('render') + ->with('@SonataUser/Form/roles_matrix.html.twig', [ + 'grouped_roles' => [ + 'fooadmin' => [ + 'BASE_ROLE_FOO_EDIT' => [ + 'role' => 'BASE_ROLE_FOO_EDIT', + 'label' => 'EDIT', + 'role_translated' => 'ROLE FOO TRANSLATED', + 'admin_label' => 'fooadmin', + 'is_granted' => true, + 'form' => $form, + ], + ], + ], + 'permission_labels' => ['EDIT', 'CREATE'], + ]) + ->willReturn(''); + + $rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder); + $rolesMatrixExtension->renderMatrix($this->environment, $this->formView); + } + + /** + * @test + */ + public function renderMatrixFormVarsNotSet(): void + { + $roles = [ + 'BASE_ROLE_FOO_%s' => [ + 'role' => 'BASE_ROLE_FOO_EDIT', + 'label' => 'EDIT', + 'role_translated' => 'ROLE FOO TRANSLATED', + 'admin_label' => 'fooadmin', + 'is_granted' => true, + ], + ]; + $this->rolesBuilder + ->expects($this->once()) + ->method('getRoles') + ->willReturn($roles); + + $this->rolesBuilder + ->expects($this->once()) + ->method('getPermissionLabels') + ->willReturn(['EDIT', 'CREATE']); + + $form = new FormView(); + $form->vars['value'] = 'WRONG_VALUE'; + + $this->formView + ->expects($this->once()) + ->method('getIterator') + ->willReturn([$form]); + + $this->environment + ->expects($this->once()) + ->method('render') + ->with('@SonataUser/Form/roles_matrix.html.twig', [ + 'grouped_roles' => [ + 'fooadmin' => [ + 'BASE_ROLE_FOO_%s' => [ + 'role' => 'BASE_ROLE_FOO_EDIT', + 'label' => 'EDIT', + 'role_translated' => 'ROLE FOO TRANSLATED', + 'admin_label' => 'fooadmin', + 'is_granted' => true, + ], + ], + ], + 'permission_labels' => ['EDIT', 'CREATE'], + ]) + ->willReturn(''); + + $rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder); + $rolesMatrixExtension->renderMatrix($this->environment, $this->formView); + } +}