Skip to content

Commit

Permalink
Roles matrix group by admin and admin group (#1587)
Browse files Browse the repository at this point in the history
Co-authored-by: Vincent Langlet <[email protected]>
Closes #1583
  • Loading branch information
piddubnij authored Feb 2, 2023
1 parent 2154120 commit e22f16d
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 42 deletions.
18 changes: 14 additions & 4 deletions src/Resources/views/Form/roles_matrix.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,26 @@ file that was distributed with this source code.
<table class="table table-condensed">
<thead>
<tr>
<th></th>
<th></th>
{% for label in permission_labels|sort %}
<th> {{ label }} </th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for admin_label, roles in grouped_roles %}
<tr>
<th>{{ admin_label }}</th>
{% for group_code, admin_roles in grouped_roles %}
{% set new_group = true %}
{% for admin_code, roles in admin_roles %}
<tr>
{% for role, attributes in roles|sort %}
{% if loop.first %}
{% if new_group %}
{% set new_group = false %}
<th rowspan="{{ admin_roles|length }}" scope="rowgroup">{{ attributes.group_label|default('') }}</th>
{% endif %}
<th>{{ attributes.admin_label|default('') }}</th>
{% endif %}
<td>
{{ form_widget(attributes.form, { label: false }) }}
{% if not attributes.is_granted %}
Expand All @@ -34,7 +43,8 @@ file that was distributed with this source code.
{% endif %}
</td>
{% endfor %}
</tr>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
Expand Down
70 changes: 54 additions & 16 deletions src/Security/RolesBuilder/AdminRolesBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

/**
* @author Silas Joisten <[email protected]>
*
* @phpstan-import-type Role from RolesBuilderInterface
*/
final class AdminRolesBuilder implements AdminRolesBuilderInterface
{
Expand Down Expand Up @@ -76,27 +78,63 @@ public function addExcludeAdmin(string $exclude): void

public function getRoles(?string $domain = null): array
{
$adminServiceCodes = array_diff($this->pool->getAdminServiceCodes(), $this->excludeAdmins);

// get groups and admins sort by config
$adminRoles = [];
foreach ($this->pool->getAdminServiceCodes() as $code) {
if (\in_array($code, $this->excludeAdmins, true)) {
continue;
}
foreach ($this->pool->getAdminGroups() as $groupCode => $group) {
foreach ($group['items'] as $item) {
if (!isset($item['admin'])) {
continue;
}

$key = array_search($item['admin'], $adminServiceCodes, true);
if (false === $key) {
continue;
}
unset($adminServiceCodes[$key]);

$admin = $this->pool->getInstance($code);
$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() ?? ''),
];
$groupLabelTranslated = $this->translator->trans($group['label'], [], $group['translation_domain']);

$adminRoles = array_merge($adminRoles, $this->getAdminRolesByAdminCode($item['admin'], $domain, $groupLabelTranslated, $groupCode));
}
}

// admin with config "show_in_dashboard" set "false" or group not set, does not have group
foreach ($adminServiceCodes as $code) {
$adminRoles = array_merge($adminRoles, $this->getAdminRolesByAdminCode($code, $domain));
}

return $adminRoles;
}

/**
* @return array<string, array<string, string|bool>>
*
* @phpstan-return array<string, Role>
*/
private function getAdminRolesByAdminCode(string $code, ?string $domain = null, string $groupLabelTranslated = '', string $groupCode = ''): array
{
$adminRoles = [];
$admin = $this->pool->getInstance($code);
$securityHandler = $admin->getSecurityHandler();
$baseRole = $securityHandler->getBaseRole($admin);
$adminLabelTranslated = $admin->getTranslator()->trans($admin->getLabel() ?? '', [], $admin->getTranslationDomain());
$isMasterAdmin = $this->isMaster($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' => $isMasterAdmin || $this->authorizationChecker->isGranted($role),
'admin_label' => $adminLabelTranslated,
'admin_code' => $code,
'group_label' => $groupLabelTranslated,
'group_code' => $groupCode,
];
}

return $adminRoles;
}

Expand Down
5 changes: 4 additions & 1 deletion src/Security/RolesBuilder/RolesBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
* role_translated: string,
* is_granted: boolean,
* label?: string,
* admin_label?: string
* admin_label?: string,
* admin_code?: string,
* group_label?: string,
* group_code?: string
* }
*/
interface RolesBuilderInterface
Expand Down
21 changes: 17 additions & 4 deletions src/Twig/RolesMatrixExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,27 @@ public function renderMatrix(Environment $environment, FormView $form): string
{
$groupedRoles = [];
foreach ($this->rolesBuilder->getRoles() as $role => $attributes) {
if (!isset($attributes['admin_label'])) {
continue;
if (!isset($attributes['admin_code'])) {
// NEXT_MAJOR: Remove those lines and uncomment the last one.
if (!isset($attributes['admin_label'])) {
continue;
}

@trigger_error(
'Not setting the "admin_code" attribute to admin role is deprecated since sonata-project/user-bundle 5.5'
.' and without it admin role will be skipped in version 6.0.',
\E_USER_DEPRECATED
);

$attributes['admin_code'] = $attributes['admin_label'];
// continue;
}

$groupedRoles[$attributes['admin_label']][$role] = $attributes;
$groupCode = $attributes['group_code'] ?? '';
$groupedRoles[$groupCode][$attributes['admin_code']][$role] = $attributes;
foreach ($form->getIterator() as $child) {
if ($child->vars['value'] === $role) {
$groupedRoles[$attributes['admin_label']][$role]['form'] = $child;
$groupedRoles[$groupCode][$attributes['admin_code']][$role]['form'] = $child;
}
}
}
Expand Down
30 changes: 28 additions & 2 deletions tests/Security/RolesBuilder/AdminRolesBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,19 @@ protected function setUp(): void
$container = new Container();
$container->set('sonata.admin.bar', $this->admin);

$this->pool = new Pool($container, ['sonata.admin.bar']);
$adminGroups = [
'bar' => [
'label' => 'Bar',
'translation_domain' => '',
'icon' => '<i class="fas fa-edit"></i>',
'items' => [['admin' => 'sonata.admin.bar', 'roles' => [], 'route_absolute' => false, 'route_params' => []]],
'keep_open' => false,
'on_top' => false,
'roles' => [],
],
];

$this->pool = new Pool($container, ['sonata.admin.bar'], $adminGroups);
$this->configuration = new SonataConfiguration('title', 'logo', [
'confirm_exit' => true,
'default_admin_route' => 'show',
Expand Down Expand Up @@ -105,7 +117,9 @@ protected function setUp(): void

public function testGetPermissionLabels(): void
{
$this->translator->method('trans');
$this->translator->method('trans')->willReturnCallback(
static fn (string $key): string => $key
);

$this->securityHandler->method('getBaseRole')
->willReturn('ROLE_SONATA_FOO_%s');
Expand Down Expand Up @@ -170,27 +184,39 @@ public function testGetRoles(): void
'role_translated' => 'ROLE_SONATA_FOO_GUEST',
'is_granted' => false,
'admin_label' => 'Foo',
'admin_code' => 'sonata.admin.bar',
'group_label' => 'Foo',
'group_code' => 'bar',
],
'ROLE_SONATA_FOO_STAFF' => [
'role' => 'ROLE_SONATA_FOO_STAFF',
'label' => 'STAFF',
'role_translated' => 'ROLE_SONATA_FOO_STAFF',
'is_granted' => false,
'admin_label' => 'Foo',
'admin_code' => 'sonata.admin.bar',
'group_label' => 'Foo',
'group_code' => 'bar',
],
'ROLE_SONATA_FOO_EDITOR' => [
'role' => 'ROLE_SONATA_FOO_EDITOR',
'label' => 'EDITOR',
'role_translated' => 'ROLE_SONATA_FOO_EDITOR',
'is_granted' => false,
'admin_label' => 'Foo',
'admin_code' => 'sonata.admin.bar',
'group_label' => 'Foo',
'group_code' => 'bar',
],
'ROLE_SONATA_FOO_ADMIN' => [
'role' => 'ROLE_SONATA_FOO_ADMIN',
'label' => 'ADMIN',
'role_translated' => 'ROLE_SONATA_FOO_ADMIN',
'is_granted' => false,
'admin_label' => 'Foo',
'admin_code' => 'sonata.admin.bar',
'group_label' => 'Foo',
'group_code' => 'bar',
],
];

Expand Down
107 changes: 92 additions & 15 deletions tests/Twig/RolesMatrixExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ public function testRenderMatrix(): void
'role_translated' => 'ROLE FOO TRANSLATED',
'admin_label' => 'fooadmin',
'is_granted' => true,
'admin_code' => 'fooadmin',
'group_label' => 'BarGroup',
'group_code' => 'bargroup',
],
];
$this->rolesBuilder
Expand All @@ -226,14 +229,80 @@ public function testRenderMatrix(): void
->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,
'bargroup' => [
'fooadmin' => [
'BASE_ROLE_FOO_EDIT' => [
'role' => 'BASE_ROLE_FOO_EDIT',
'label' => 'EDIT',
'role_translated' => 'ROLE FOO TRANSLATED',
'admin_label' => 'fooadmin',
'is_granted' => true,
'admin_code' => 'fooadmin',
'group_label' => 'BarGroup',
'group_code' => 'bargroup',
'form' => $form,
],
],
],
],
'permission_labels' => ['EDIT', 'CREATE'],
])
->willReturn('');

$rolesMatrixExtension = new RolesMatrixExtension($this->rolesBuilder);
$rolesMatrixExtension->renderMatrix($this->environment, $this->formView);
}

/**
* NEXT_MAJOR: Remove this test.
*
* @group legacy
*/
public function testRenderMatrixWithoutAdminCode(): 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(static::once())
->method('getRoles')
->willReturn($roles);

$this->rolesBuilder
->expects(static::once())
->method('getPermissionLabels')
->willReturn(['EDIT', 'CREATE']);

$form = new FormView();
$form->vars['value'] = 'BASE_ROLE_FOO_EDIT';

$this->formView
->expects(static::once())
->method('getIterator')
->willReturn(new \ArrayIterator([$form]));

$this->environment
->expects(static::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,
'admin_code' => 'fooadmin',
'form' => $form,
],
],
],
],
Expand All @@ -254,6 +323,9 @@ public function testRenderMatrixFormVarsNotSet(): void
'role_translated' => 'ROLE FOO TRANSLATED',
'admin_label' => 'fooadmin',
'is_granted' => true,
'admin_code' => 'fooadmin',
'group_label' => 'BarGroup',
'group_code' => 'bargroup',
],
];
$this->rolesBuilder
Expand All @@ -279,13 +351,18 @@ public function testRenderMatrixFormVarsNotSet(): void
->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,
'bargroup' => [
'fooadmin' => [
'BASE_ROLE_FOO_%s' => [
'role' => 'BASE_ROLE_FOO_EDIT',
'label' => 'EDIT',
'role_translated' => 'ROLE FOO TRANSLATED',
'admin_label' => 'fooadmin',
'is_granted' => true,
'admin_code' => 'fooadmin',
'group_label' => 'BarGroup',
'group_code' => 'bargroup',
],
],
],
],
Expand Down

0 comments on commit e22f16d

Please sign in to comment.