refactored permissions for simpler customizations (#808)

This commit is contained in:
Kevin Papst
2019-05-25 17:23:15 +02:00
committed by GitHub
parent d18019159b
commit ebff4a765a
24 changed files with 299 additions and 161 deletions

View File

@@ -28,10 +28,16 @@ New permissions are available. If you configured custom permissions in `local.ya
- `view_tag` - view all tags - `view_tag` - view all tags
- `delete_tag` - delete tags - `delete_tag` - delete tags
- `edit_exported_timesheet` - allows to edit records which were exported - `edit_exported_timesheet` - allows to edit records which were exported
- `role_permissions` - view calculated permissions for user roles
Removed permission:
- `system_actions` - removed experimental feature to flush your cache from the about screen
### BC BREAKS ### BC BREAKS
- API: Format for queries including a datetime object fixed, finally using the HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`) - API: Format for queries including a datetime object fixed, finally using the HTML5 format (previously `2019-03-02 14:23` - now `2019-03-02T14:23:00`)
- **Permission config**: the `permissions` definition in your `local.yaml` needs to be verified/changed, as the internal structure was highly optimized to simplify the definition.
Thanks to the new structure, you should be able to remove almost everything from your `local.yaml`: please read [the updated permission docu](https://www.kimai.org/documentation/permissions.html).
## [0.9](https://github.com/kevinpapst/kimai2/releases/tag/0.9) ## [0.9](https://github.com/kevinpapst/kimai2/releases/tag/0.9)

View File

@@ -39,8 +39,10 @@ export default class KimaiAPILink extends KimaiClickHandlerReducedInTableRow {
} }
if (attributes.question !== undefined) { if (attributes.question !== undefined) {
self.getContainer().getPlugin('alert').question(attributes.question, function() { self.getContainer().getPlugin('alert').question(attributes.question, function(value) {
self._callApi(url, attributes); if (value) {
self._callApi(url, attributes);
}
}); });
} else { } else {
self._callApi(url, attributes); self._callApi(url, attributes);

View File

@@ -41,8 +41,6 @@ export default class KimaiAlert extends KimaiPlugin {
} }
Swal.fire({ Swal.fire({
//toast: true,
//timer: 3000,
timer: 1500, timer: 1500,
position: 'top-end', position: 'top-end',
showConfirmButton: false, showConfirmButton: false,
@@ -71,9 +69,7 @@ export default class KimaiAlert extends KimaiPlugin {
confirmButtonText: translation.get('confirm'), confirmButtonText: translation.get('confirm'),
cancelButtonText: translation.get('cancel') cancelButtonText: translation.get('cancel')
}).then((result) => { }).then((result) => {
if (result.value) { callback(result.value);
callback(result.value);
}
}); });
} }

View File

@@ -75,34 +75,38 @@ kimai:
# https://www.kimai.org/documentation/permissions.html # https://www.kimai.org/documentation/permissions.html
# -------------------------------------------------------------------------------- # --------------------------------------------------------------------------------
permissions: permissions:
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions and sets"])
sets: sets:
# mapping complex rule sets of single permissions to named "sets" ("set name" = [array of "permissions"]) ACTIVITIES: ['view_activity','create_activity','edit_activity','delete_activity']
ACTIVITIES: [view_activity,create_activity,edit_activity,delete_activity] PROJECTS: ['view_project','create_project','edit_project','delete_project']
PROJECTS: [view_project,create_project,edit_project,delete_project] CUSTOMERS: ['view_customer','create_customer','edit_customer','delete_customer']
CUSTOMERS: [view_customer,create_customer,edit_customer,delete_customer] INVOICE: ['view_invoice','create_invoice']
INVOICE: [view_invoice,create_invoice] INVOICE_TEMPLATE: ['view_invoice_template','create_invoice_template','edit_invoice_template','delete_invoice_template']
INVOICE_TEMPLATE: [view_invoice_template,create_invoice_template,edit_invoice_template,delete_invoice_template] TIMESHEET: ['view_own_timesheet','start_own_timesheet','stop_own_timesheet','create_own_timesheet','edit_own_timesheet','export_own_timesheet','delete_own_timesheet']
TIMESHEET: [view_own_timesheet,start_own_timesheet,stop_own_timesheet,create_own_timesheet,edit_own_timesheet,export_own_timesheet,delete_own_timesheet] TIMESHEET_OTHER: ['view_other_timesheet','start_other_timesheet','stop_other_timesheet','create_other_timesheet','edit_other_timesheet','export_other_timesheet','delete_other_timesheet']
TIMESHEET_OTHER: [view_other_timesheet,start_other_timesheet,stop_other_timesheet,create_other_timesheet,edit_other_timesheet,export_other_timesheet,delete_other_timesheet] PROFILE: ['view_own_profile','edit_own_profile','password_own_profile','preferences_own_profile','api-token_own_profile']
PROFILE: [view_own_profile,edit_own_profile,password_own_profile,preferences_own_profile,api-token_own_profile] PROFILE_OTHER: ['view_other_profile','edit_other_profile','delete_other_profile','password_other_profile','roles_other_profile','preferences_other_profile','api-token_other_profile']
PROFILE_OTHER: [view_other_profile,edit_other_profile,delete_other_profile,password_other_profile,roles_other_profile,preferences_other_profile,api-token_other_profile] TAGS: ['view_tag','delete_tag']
TAGS: [view_tag,delete_tag] USER: ['view_user','create_user','delete_user','role_permissions']
USER: [view_user,create_user,delete_user] RATE: ['view_rate_own_timesheet','edit_rate_own_timesheet']
RATE: [view_rate_own_timesheet,edit_rate_own_timesheet] RATE_OTHER: ['view_rate_other_timesheet','edit_rate_other_timesheet']
RATE_OTHER: [view_rate_other_timesheet,edit_rate_other_timesheet] EXPORT: ['view_export','create_export','edit_export_own_timesheet','edit_export_other_timesheet']
EXPORT: [view_export,create_export,edit_export_own_timesheet,edit_export_other_timesheet] ROLE_USER: ['@TIMESHEET','@PROFILE']
ROLE_TEAMLEAD: ['@INVOICE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@EXPORT','@TAGS','@INVOICE_TEMPLATE','!delete_invoice_template','view_rate_own_timesheet','view_rate_other_timesheet','hourly-rate_own_profile']
ROLE_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@RATE','@RATE_OTHER','@EXPORT','@TAGS','hourly-rate_own_profile','edit_exported_timesheet']
ROLE_SUPER_ADMIN: ['@ACTIVITIES','@PROJECTS','@CUSTOMERS','@INVOICE','@INVOICE_TEMPLATE','@TIMESHEET','@TIMESHEET_OTHER','@PROFILE','@PROFILE_OTHER','@USER','@RATE','@RATE_OTHER','@EXPORT','@TAGS','hourly-rate_own_profile','hourly-rate_other_profile','delete_own_profile','roles_own_profile','system_information','system_configuration','plugins','edit_exported_timesheet']
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
maps: maps:
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"]) ROLE_USER: ['ROLE_USER']
ROLE_USER: [TIMESHEET,PROFILE] ROLE_TEAMLEAD: ['ROLE_TEAMLEAD']
ROLE_TEAMLEAD: [INVOICE,TIMESHEET,TIMESHEET_OTHER,PROFILE,EXPORT,TAGS] ROLE_ADMIN: ['ROLE_ADMIN']
ROLE_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,RATE,RATE_OTHER,EXPORT,TAGS] ROLE_SUPER_ADMIN: ['ROLE_SUPER_ADMIN']
ROLE_SUPER_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,PROFILE_OTHER,USER,RATE,RATE_OTHER,EXPORT,TAGS] # add or remove single permissions
roles: roles:
# adding single permissions to user roles, extending the definition from "sets" ("role name" = [array of "permissions"])
ROLE_USER: [] ROLE_USER: []
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile] ROLE_TEAMLEAD: []
ROLE_ADMIN: [hourly-rate_own_profile,edit_exported_timesheet] ROLE_ADMIN: []
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information,system_actions,system_configuration,plugins,edit_exported_timesheet] ROLE_SUPER_ADMIN: []
# -------------------------------------------------------------------------------- # --------------------------------------------------------------------------------

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{ {
"build/app.js": "./app.js?376d50667e3784222761", "build/app.js": "./app.js?7f61ab551431df41b451",
"build/app.css": "./app.css?954c34ace3717cfb9d9c80d2be5f8c68", "build/app.css": "./app.css?954c34ace3717cfb9d9c80d2be5f8c68",
"build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29", "build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29",
"build/images/fa-solid-900.svg": "./images/fa-solid-900.svg?666a82cb", "build/images/fa-solid-900.svg": "./images/fa-solid-900.svg?666a82cb",

View File

@@ -11,10 +11,6 @@ namespace App\Controller;
use App\Constants; use App\Constants;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
/** /**
@@ -43,11 +39,6 @@ class AboutController extends AbstractController
* @return \Symfony\Component\HttpFoundation\Response * @return \Symfony\Component\HttpFoundation\Response
*/ */
public function debugAction() public function debugAction()
{
return $this->getAboutView();
}
protected function getAboutView(array $additional = [])
{ {
$phpInfo = $this->getPhpInfo(); $phpInfo = $this->getPhpInfo();
unset($phpInfo[0]); unset($phpInfo[0]);
@@ -89,8 +80,7 @@ class AboutController extends AbstractController
], ],
'info' => $phpInfo, 'info' => $phpInfo,
'settings' => $settings, 'settings' => $settings,
], ]
$additional
)); ));
} }
@@ -119,28 +109,6 @@ class AboutController extends AbstractController
]); ]);
} }
/**
* @Route(path="/flush-cache", name="system_flush_cache", methods={"GET"})
*
* @Security("is_granted('system_actions')")
*/
public function rebuildContainer(KernelInterface $kernel)
{
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'cache:clear',
'--env' => $kernel->getEnvironment(),
'-n',
]);
$output = new BufferedOutput();
$application->run($input, $output);
return $this->getAboutView(['content_action' => $output->fetch()]);
}
/** /**
* @author https://php.net/manual/en/function.phpinfo.php#117961 * @author https://php.net/manual/en/function.phpinfo.php#117961
* @return array * @return array

View File

@@ -14,6 +14,7 @@ use App\Entity\User;
use App\Form\Toolbar\UserToolbarForm; use App\Form\Toolbar\UserToolbarForm;
use App\Form\UserCreateType; use App\Form\UserCreateType;
use App\Repository\Query\UserQuery; use App\Repository\Query\UserQuery;
use App\Security\RolePermissionManager;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -173,6 +174,22 @@ class UserController extends AbstractController
); );
} }
/**
* @Route(path="/permissions", name="admin_user_permissions", methods={"GET", "POST"})
* @Security("is_granted('role_permissions')")
*
* @param RolePermissionManager $manager
* @return \Symfony\Component\HttpFoundation\Response
*/
public function permissions(RolePermissionManager $manager)
{
return $this->render('user/permissions.html.twig', [
'roles' => $manager->getRoles(),
'permissions' => $manager->getPermissions(),
'manager' => $manager,
]);
}
/** /**
* @param UserQuery $query * @param UserQuery $query
* @return \Symfony\Component\Form\FormInterface * @return \Symfony\Component\Form\FormInterface

View File

@@ -71,14 +71,8 @@ class AppExtension extends Extension
*/ */
protected function createPermissionParameter(array $config, ContainerBuilder $container) protected function createPermissionParameter(array $config, ContainerBuilder $container)
{ {
$roles = [];
foreach ($config['maps'] as $role => $sets) { foreach ($config['maps'] as $role => $sets) {
if (!isset($config['roles'][$role])) {
$exception = new InvalidConfigurationException(
'Configured permission set includes unknown role "' . $role . '"'
);
$exception->setPath('kimai.permissions.maps.' . $role);
throw $exception;
}
foreach ($sets as $set) { foreach ($sets as $set) {
if (!isset($config['sets'][$set])) { if (!isset($config['sets'][$set])) {
$exception = new InvalidConfigurationException( $exception = new InvalidConfigurationException(
@@ -87,12 +81,59 @@ class AppExtension extends Extension
$exception->setPath('kimai.permissions.maps.' . $role); $exception->setPath('kimai.permissions.maps.' . $role);
throw $exception; throw $exception;
} }
$config['roles'][$role] = array_unique(array_merge($config['roles'][$role], $config['sets'][$set])); $roles[$role] = array_merge($roles[$role] ?? [], $this->getFilteredPermissions(
$this->extractSinglePermissionsFromSet($config, $set)
));
} }
} }
// delete forbidden permissions from roles
foreach (array_keys($config['maps']) as $name) {
$config['roles'][$name] = $this->getFilteredPermissions(
array_unique(array_merge($roles[$name], $config['roles'][$name] ?? []))
);
}
$container->setParameter('kimai.permissions', $config['roles']); $container->setParameter('kimai.permissions', $config['roles']);
} }
protected function getFilteredPermissions(array $permissions): array
{
$deleteFromArray = array_filter($permissions, function ($permission) {
return $permission[0] == '!';
});
return array_filter($permissions, function ($permission) use ($deleteFromArray) {
if ($permission[0] == '!') {
return false;
}
return !in_array('!' . $permission, $deleteFromArray);
});
}
protected function extractSinglePermissionsFromSet(array $permissions, string $name): array
{
if (!isset($permissions['sets'][$name])) {
throw new InvalidConfigurationException('Unknown permission set "' . $name . '"');
}
$result = [];
foreach ($permissions['sets'][$name] as $permissionName) {
if ($permissionName[0] == '@') {
$result = array_merge(
$result,
$this->extractSinglePermissionsFromSet($permissions, substr($permissionName, 1))
);
} else {
$result[] = $permissionName;
}
}
return $result;
}
/** /**
* @param array $config * @param array $config
* @param ContainerBuilder $container * @param ContainerBuilder $container

View File

@@ -434,7 +434,6 @@ class Configuration implements ConfigurationInterface
->arrayNode('sets') ->arrayNode('sets')
->requiresAtLeastOneElement() ->requiresAtLeastOneElement()
->useAttributeAsKey('key') ->useAttributeAsKey('key')
->performNoDeepMerging()
->arrayPrototype() ->arrayPrototype()
->useAttributeAsKey('key') ->useAttributeAsKey('key')
->isRequired() ->isRequired()
@@ -445,7 +444,6 @@ class Configuration implements ConfigurationInterface
->arrayNode('maps') ->arrayNode('maps')
->requiresAtLeastOneElement() ->requiresAtLeastOneElement()
->useAttributeAsKey('key') ->useAttributeAsKey('key')
->performNoDeepMerging()
->arrayPrototype() ->arrayPrototype()
->useAttributeAsKey('key') ->useAttributeAsKey('key')
->isRequired() ->isRequired()
@@ -456,13 +454,18 @@ class Configuration implements ConfigurationInterface
->arrayNode('roles') ->arrayNode('roles')
->requiresAtLeastOneElement() ->requiresAtLeastOneElement()
->useAttributeAsKey('key') ->useAttributeAsKey('key')
->performNoDeepMerging()
->arrayPrototype() ->arrayPrototype()
->useAttributeAsKey('key') ->useAttributeAsKey('key')
->isRequired() ->isRequired()
->prototype('scalar')->end() ->prototype('scalar')->end()
->defaultValue([]) ->defaultValue([])
->end() ->end()
->defaultValue([
'ROLE_USER' => [],
'ROLE_TEAMLEAD' => [],
'ROLE_ADMIN' => [],
'ROLE_SUPER_ADMIN' => [],
])
->end() ->end()
->end() ->end()
; ;

View File

@@ -9,6 +9,7 @@
namespace App\Form; namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
@@ -55,7 +56,7 @@ class UserCreateType extends UserEditType
public function __configureOptions(OptionsResolver $resolver) public function __configureOptions(OptionsResolver $resolver)
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'class' => 'Kimai:User', 'class' => User::class,
]); ]);
} }
} }

View File

@@ -16,13 +16,10 @@ class RolePermissionManager
*/ */
protected $permissions = []; protected $permissions = [];
/** /**
* @var array * @var string[]
*/ */
protected $knownPermissions = []; protected $knownPermissions = [];
/**
* @param array $permissions
*/
public function __construct(array $permissions) public function __construct(array $permissions)
{ {
$this->permissions = $permissions; $this->permissions = $permissions;
@@ -33,30 +30,12 @@ class RolePermissionManager
$this->knownPermissions = array_unique($this->knownPermissions); $this->knownPermissions = array_unique($this->knownPermissions);
} }
/** public function isRegisteredPermission(string $permission): bool
* @param string $permission
* @return bool
*/
public function isRegisteredPermission($permission)
{ {
return in_array($permission, $this->knownPermissions); return in_array($permission, $this->knownPermissions);
} }
/** public function hasPermission(string $role, string $permission): bool
* @param string $role
* @return bool
*/
public function roleHasPermission($role)
{
return isset($this->permissions[$role]);
}
/**
* @param string $role
* @param string $permission
* @return bool
*/
public function hasPermission($role, $permission)
{ {
if (!isset($this->permissions[$role])) { if (!isset($this->permissions[$role])) {
return false; return false;
@@ -64,4 +43,19 @@ class RolePermissionManager
return in_array($permission, $this->permissions[$role]); return in_array($permission, $this->permissions[$role]);
} }
public function getRoles(): array
{
return array_keys($this->permissions);
}
public function roleHasPermission(string $role): bool
{
return isset($this->permissions[$role]);
}
public function getPermissions(): array
{
return $this->knownPermissions;
}
} }

View File

@@ -94,6 +94,8 @@ class Extensions extends AbstractExtension
'profile-stats' => 'far fa-chart-bar', 'profile-stats' => 'far fa-chart-bar',
'profile' => 'fas fa-user-edit', 'profile' => 'fas fa-user-edit',
'warning' => 'fas fa-exclamation-triangle', 'warning' => 'fas fa-exclamation-triangle',
'permissions' => 'fas fa-user-lock',
'back' => 'fas fa-long-arrow-alt-left',
]; ];
/** /**

View File

@@ -4,22 +4,6 @@
{% block page_subtitle %}{{ 'about.subtitle'|trans({}, 'about') }}{% endblock %} {% block page_subtitle %}{{ 'about.subtitle'|trans({}, 'about') }}{% endblock %}
{% block about_box %} {% block about_box %}
{% if is_granted('system_actions') %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'boxtype': 'warning'} %}
{% block box_title %}{{ 'label.actions'|trans }}{% endblock %}
{% block box_body %}
{% if content_action is defined and content_action is not empty %}
<pre>
{{ content_action }}
</pre>
{% endif %}
<p>
<a href="{{ path('system_flush_cache') }}" class="btn btn-primary" onclick="$(this).html('Clear cache <i class=\'fas fa-spinner fa-spin\'></i>')">Clear cache</a>
</p>
{% endblock %}
{% endembed %}
{% endif %}
{% endblock %} {% endblock %}
{% block about %} {% block about %}

View File

@@ -131,6 +131,9 @@
} }
ALERT.question(message, function(value) { ALERT.question(message, function(value) {
if (!value) {
return;
}
var btn = $(button); var btn = $(button);
var exportButtons = $('.exportBtn'); var exportButtons = $('.exportBtn');
// disabling does not yet work... // disabling does not yet work...

View File

@@ -2,6 +2,7 @@
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/toolbar.html.twig" as toolbar %} {% import "macros/toolbar.html.twig" as toolbar %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/actions.html.twig" as actions %}
{% set columns = { {% set columns = {
'date': 'alwaysVisible', 'date': 'alwaysVisible',
@@ -16,16 +17,7 @@
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %} {% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %} {% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %}
{% block page_actions %} {% block page_actions %}{{ actions.invoices('index') }}{% endblock %}
{% set actions = {'filter': '#collapseInvoice', 'visibility': '#modal_invoice'} %}
{% if is_granted('create_invoice_template') %}
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
{% endif %}
{% if is_granted('view_invoice_template') %}
{% set actions = actions|merge({'list': path('admin_invoice_template')}) %}
{% endif %}
{{ widgets.page_actions(actions) }}
{% endblock %}
{% block main_before %} {% block main_before %}
{{ toolbar.toolbar(form, 'collapseInvoice', true) }} {{ toolbar.toolbar(form, 'collapseInvoice', true) }}

View File

@@ -1,21 +1,11 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% import "macros/datatables.html.twig" as tables %} {% import "macros/datatables.html.twig" as tables %}
{% import "macros/actions.html.twig" as actions %}
{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %} {% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'admin_invoice_template.subtitle'|trans }}{% endblock %} {% block page_subtitle %}{{ 'admin_invoice_template.subtitle'|trans }}{% endblock %}
{% block page_actions %} {% block page_actions %}{{ actions.invoice_templates('index') }}{% endblock %}
{# TODO move me to actions macro #}
{% set actions = {} %}
{% if is_granted('create_invoice_template') %}
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
{% endif %}
{% if is_granted('view_invoice') %}
{% set actions = actions|merge({'invoice': path('invoice')}) %}
{% endif %}
{% set event = trigger('actions.invoice_templates', {'actions': actions, 'view': 'index'}) %}
{{ widgets.page_actions(actions) }}
{% endblock %}
{% block main %} {% block main %}
{% if entries.count == 0 %} {% if entries.count == 0 %}

View File

@@ -48,16 +48,43 @@
{% macro users(view) %} {% macro users(view) %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% set actions = {'filter': '#collapseUserAdmin', 'visibility': '#modal_user_admin'} %} {% set actions = {} %}
{% if view == 'index' %}
{% set actions = actions|merge({'filter': '#collapseUserAdmin', 'visibility': '#modal_user_admin'}) %}
{% else %}
{% set actions = actions|merge({'back': path('admin_user')}) %}
{% endif %}
{% if view != 'permissions' and is_granted('role_permissions') %}
{% set actions = actions|merge({'permissions': path('admin_user_permissions')}) %}
{% endif %}
{% if is_granted('create_user') %} {% if is_granted('create_user') %}
{% set actions = actions|merge({'create': path('admin_user_create')}) %} {% set actions = actions|merge({'create': path('admin_user_create')}) %}
{% endif %} {% endif %}
{% if view == 'index' %}
{% set actions = actions|merge({'help': {'url': 'users.html'|docu_link, 'target': '_blank'}}) %}
{% elseif view == 'permissions' %}
{% set actions = actions|merge({'help': {'url': 'permissions.html'|docu_link, 'target': '_blank'}}) %}
{% endif %}
{% set event = trigger('actions.users', {'actions': actions, 'view': view}) %} {% set event = trigger('actions.users', {'actions': actions, 'view': view}) %}
{{ widgets.page_actions(event.payload.actions) }} {{ widgets.page_actions(event.payload.actions) }}
{% endmacro %} {% endmacro %}
{% macro user_permissions(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set actions = {} %}
{% set actions = actions|merge({'back': path('admin_user')}) %}
{% set actions = actions|merge({'help': {'url': 'permissions.html'|docu_link, 'target': '_blank'}}) %}
{% set event = trigger('actions.user_permissions', {'actions': actions, 'view': view}) %}
{{ widgets.page_actions(event.payload.actions) }}
{% endmacro %}
{% macro user(user, view) %} {% macro user(user, view) %}
{% import "macros/widgets.html.twig" as widgets %} {% import "macros/widgets.html.twig" as widgets %}
{% set actions = {} %} {% set actions = {} %}
@@ -371,3 +398,39 @@
{% set event = trigger('actions.tag', {'actions': actions, 'view': view, 'tag': tag}) %} {% set event = trigger('actions.tag', {'actions': actions, 'view': view, 'tag': tag}) %}
{{ widgets.table_actions(event.payload.actions) }} {{ widgets.table_actions(event.payload.actions) }}
{% endmacro %} {% endmacro %}
{% macro invoices(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set actions = {'filter': '#collapseInvoice', 'visibility': '#modal_invoice'} %}
{% if is_granted('create_invoice_template') %}
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
{% endif %}
{% if is_granted('view_invoice_template') %}
{% set actions = actions|merge({'list': path('admin_invoice_template')}) %}
{% endif %}
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}
{% set event = trigger('actions.invoices', {'actions': actions, 'view': view}) %}
{{ widgets.page_actions(event.payload.actions) }}
{% endmacro %}
{% macro invoice_templates(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set actions = {} %}
{% if is_granted('view_invoice') %}
{% set actions = actions|merge({'back': path('invoice')}) %}
{% endif %}
{% if is_granted('create_invoice_template') %}
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
{% endif %}
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}
{% set event = trigger('actions.invoice_templates', {'actions': actions, 'view': 'index'}) %}
{{ widgets.page_actions(actions) }}
{% endmacro %}

View File

@@ -70,22 +70,36 @@
{% endspaceless %} {% endspaceless %}
{% endmacro %} {% endmacro %}
{% macro data_table_header_options(name, columns, options) %}
{% set skipStripped = options.skipStripped|default(false) %}
{% set reloadEvent = options.reloadEvent|default('') %}
{% set translationDomain = options.translationDomain|default('messages') %}
{# |default does not work here, as the prefix might be an empty string #}
{% set translationPrefix = 'label.' %}
{% if options.translationPrefix is defined %}
{% set translationPrefix = options.translationPrefix %}
{% endif %}
{% import _self as macro %}
<div class="box box-{{ admin_lte_context.widget.type }} data_table" id="datatable_{{ name }}">
<div class="box-body no-padding">
<div class="dataTables_wrapper form-inline dt-bootstrap">
<div class="row">
<div class="col-sm-12">
<table class="table {% if not skipStripped %}table-striped {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}">
<thead>
<tr>
{%- for title, class in columns -%}
<th data-field="{{ title }}" class="{{ macro.data_table_column_class(name, columns, title) }}">{% if title is not empty and title != 'actions' %}{{ (translationPrefix ~ title)|trans({}, translationDomain) }}{% endif %}</th>
{%- endfor -%}
</tr>
</thead>
<tbody>
{% endmacro %}
{% macro data_table_header(name, columns, skipStripped, reloadEvent) %} {% macro data_table_header(name, columns, skipStripped, reloadEvent) %}
{% import _self as macro %} {% import _self as macro %}
<div class="box box-{{ admin_lte_context.widget.type }} data_table" id="datatable_{{ name }}"> {{ macro.data_table_header_options(name, columns, {'skipStripped': skipStripped, 'reloadEvent': reloadEvent}) }}
<div class="box-body no-padding">
<div class="dataTables_wrapper form-inline dt-bootstrap">
<div class="row">
<div class="col-sm-12">
<table class="table {% if not skipStripped %}table-striped {% endif %}table-hover dataTable" role="grid" data-reload-event="{{ reloadEvent }}">
<thead>
<tr>
{%- for title, class in columns -%}
<th data-field="{{ title }}" class="{{ macro.data_table_column_class(name, columns, title) }}">{% if title is not empty and title != 'actions' %}{{ ('label.' ~ title)|trans }}{% endif %}</th>
{%- endfor -%}
</tr>
</thead>
<tbody>
{% endmacro %} {% endmacro %}
{% macro data_table_footer(entries, route) %} {% macro data_table_footer(entries, route) %}

View File

@@ -24,7 +24,7 @@
<h2 class="page-header">{{ title|trans }}</h2> <h2 class="page-header">{{ title|trans }}</h2>
{% endmacro %} {% endmacro %}
{% macro label_visible(visible) %} {% macro label_boolean(visible) %}
{% import _self as macro %} {% import _self as macro %}
{% if visible %} {% if visible %}
{{ macro.label('yes', 'success') }} {{ macro.label('yes', 'success') }}
@@ -33,6 +33,11 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro label_visible(visible) %}
{% import _self as macro %}
{{ macro.label_boolean(visible) }}
{% endmacro %}
{% macro label_role(role) %} {% macro label_role(role) %}
{% import _self as macro %} {% import _self as macro %}
{% if role == 'ROLE_SUPER_ADMIN' %} {% if role == 'ROLE_SUPER_ADMIN' %}

View File

@@ -0,0 +1,40 @@
{% extends 'base.html.twig' %}
{% import "macros/datatables.html.twig" as tables %}
{% import "macros/widgets.html.twig" as widgets %}
{% import "macros/toolbar.html.twig" as toolbar %}
{% import "macros/actions.html.twig" as actions %}
{% set columns = {
'label.name': 'alwaysVisible',
} %}
{% for role in roles %}
{% set columns = columns|merge({
(role): 'alwaysVisible text-center',
}) %}
{% endfor %}
{% set tableName = 'user_admin_permissions' %}
{% block page_title %}{{ 'admin_user.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'admin_user.subtitle'|trans }}{% endblock %}
{% block page_actions %}{{ actions.user_permissions('index') }}{% endblock %}
{% block main %}
{{ tables.data_table_header_options(tableName, columns, {'translationPrefix': ''}) }}
{% for permission in permissions|sort %}
<tr>
<td>{{ permission }}</td>
{% for role in roles %}
<td class="text-center">
{{ widgets.label_boolean(manager.permission(role, permission)) }}
</td>
{% endfor %}
</tr>
{% endfor %}
{{ tables.data_table_footer(permissions) }}
{% endblock %}

View File

@@ -45,9 +45,7 @@ class AboutControllerTest extends ControllerBaseTest
$this->assertContains('<h3 class="box-title">PHP</h3>', $content); $this->assertContains('<h3 class="box-title">PHP</h3>', $content);
$this->assertContains('<h3 class="box-title">Server</h3>', $content); $this->assertContains('<h3 class="box-title">Server</h3>', $content);
$this->assertContains('Actions', $content);
$this->assertContains('', $content); $this->assertContains('', $content);
$this->assertContains('PHP', $content); $this->assertContains('PHP', $content);
$this->assertContains('<a href="/en/about/flush-cache"', $content);
} }
} }

View File

@@ -29,6 +29,7 @@ class UserControllerTest extends ControllerBaseTest
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN); $client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/admin/user/'); $this->assertAccessIsGranted($client, '/admin/user/');
$this->assertHasDataTable($client); $this->assertHasDataTable($client);
$this->assertDataTableRowCount($client, 'datatable_user_admin', 5);
} }
public function testCreateAction() public function testCreateAction()
@@ -196,4 +197,18 @@ class UserControllerTest extends ControllerBaseTest
], ],
]; ];
} }
public function testPermissionsIsSecure()
{
$this->assertUrlIsSecured('/admin/user/permissions');
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/user/permissions');
}
public function testPermissions()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/admin/user/permissions');
$this->assertHasDataTable($client);
$this->assertDataTableRowCount($client, 'datatable_user_admin_permissions', 66);
}
} }

View File

@@ -80,7 +80,7 @@ abstract class AbstractVoterTest extends TestCase
$roleUser = []; $roleUser = [];
$roleTeamlead = ['view_rate_own_timesheet', 'view_rate_other_timesheet', 'hourly-rate_own_profile']; $roleTeamlead = ['view_rate_own_timesheet', 'view_rate_other_timesheet', 'hourly-rate_own_profile'];
$roleAdmin = ['hourly-rate_own_profile', 'edit_exported_timesheet']; $roleAdmin = ['hourly-rate_own_profile', 'edit_exported_timesheet'];
$roleSuperAdmin = ['hourly-rate_own_profile', 'hourly-rate_other_profile', 'delete_own_profile', 'roles_own_profile', 'system_information', 'system_actions', 'system_configuration', 'plugins', 'edit_exported_timesheet']; $roleSuperAdmin = ['hourly-rate_own_profile', 'hourly-rate_other_profile', 'delete_own_profile', 'roles_own_profile', 'system_information', 'system_configuration', 'plugins', 'edit_exported_timesheet'];
$permissions = [ $permissions = [
'ROLE_USER' => array_merge($timesheet, $profile, $roleUser), 'ROLE_USER' => array_merge($timesheet, $profile, $roleUser),