roles: Restructure form and utilize class `RolesConfig`

This commit is contained in:
Johannes Meyer 2019-07-23 10:26:34 +02:00
parent 9de9fe8f39
commit 8310d9c781
2 changed files with 300 additions and 330 deletions

View File

@ -4,12 +4,10 @@
namespace Icinga\Controllers; namespace Icinga\Controllers;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Exception\AlreadyExistsException; use Icinga\Authentication\RolesConfig;
use Icinga\Exception\NotFoundError; use Icinga\Exception\NotFoundError;
use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Forms\Security\RoleForm; use Icinga\Forms\Security\RoleForm;
use Icinga\Web\Controller\AuthBackendController; use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\Notification;
/** /**
* Manage user permissions and restrictions based on roles * Manage user permissions and restrictions based on roles
@ -45,28 +43,13 @@ class RoleController extends AuthBackendController
public function addAction() public function addAction()
{ {
$this->assertPermission('config/authentication/roles/add'); $this->assertPermission('config/authentication/roles/add');
$role = new RoleForm(array(
'onSuccess' => function (RoleForm $role) { $role = new RoleForm();
$name = $role->getElement('name')->getValue(); $role->setRedirectUrl('role/list');
$values = $role->getValues(); $role->setRepository(new RolesConfig());
try { $role->setSubmitLabel($this->translate('Create Role'));
$role->add($name, $values); $role->add()->handleRequest();
} catch (AlreadyExistsException $e) {
$role->addError($e->getMessage());
return false;
}
if ($role->save()) {
Notification::success(t('Role created'));
return true;
}
return false;
}
));
$role
->setSubmitLabel($this->translate('Create Role'))
->setIniConfig(Config::app('roles', true))
->setRedirectUrl('role/list')
->handleRequest();
$this->renderForm($role, $this->translate('New Role')); $this->renderForm($role, $this->translate('New Role'));
} }
@ -78,35 +61,20 @@ class RoleController extends AuthBackendController
public function editAction() public function editAction()
{ {
$this->assertPermission('config/authentication/roles/edit'); $this->assertPermission('config/authentication/roles/edit');
$name = $this->params->getRequired('role'); $name = $this->params->getRequired('role');
$role = new RoleForm(); $role = new RoleForm();
$role->setRedirectUrl('role/list');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Update Role')); $role->setSubmitLabel($this->translate('Update Role'));
$role->edit($name);
try { try {
$role $role->handleRequest();
->setIniConfig(Config::app('roles', true))
->load($name);
} catch (NotFoundError $e) { } catch (NotFoundError $e) {
$this->httpNotFound($e->getMessage()); $this->httpNotFound($this->translate('Role not found'));
} }
$role
->setOnSuccess(function (RoleForm $role) use ($name) {
$oldName = $name;
$name = $role->getElement('name')->getValue();
$values = $role->getValues();
try {
$role->update($name, $values, $oldName);
} catch (NotFoundError $e) {
$role->addError($e->getMessage());
return false;
}
if ($role->save()) {
Notification::success(t('Role updated'));
return true;
}
return false;
})
->setRedirectUrl('role/list')
->handleRequest();
$this->renderForm($role, $this->translate('Update Role')); $this->renderForm($role, $this->translate('Update Role'));
} }
@ -116,35 +84,21 @@ class RoleController extends AuthBackendController
public function removeAction() public function removeAction()
{ {
$this->assertPermission('config/authentication/roles/remove'); $this->assertPermission('config/authentication/roles/remove');
$name = $this->params->getRequired('role'); $name = $this->params->getRequired('role');
$role = new RoleForm(); $role = new RoleForm();
$role->setRedirectUrl('role/list');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Remove Role'));
$role->remove($name);
try { try {
$role $role->handleRequest();
->setIniConfig(Config::app('roles', true))
->load($name);
} catch (NotFoundError $e) { } catch (NotFoundError $e) {
$this->httpNotFound($e->getMessage()); $this->httpNotFound($this->translate('Role not found'));
} }
$confirmation = new ConfirmRemovalForm(array(
'onSuccess' => function (ConfirmRemovalForm $confirmation) use ($name, $role) { $this->renderForm($role, $this->translate('Remove Role'));
try {
$role->remove($name);
} catch (NotFoundError $e) {
Notification::error($e->getMessage());
return false;
}
if ($role->save()) {
Notification::success(t('Role removed'));
return true;
}
return false;
}
));
$confirmation
->setSubmitLabel($this->translate('Remove Role'))
->setRedirectUrl('role/list')
->handleRequest();
$this->renderForm($confirmation, $this->translate('Remove Role'));
} }
/** /**

View File

@ -3,305 +3,321 @@
namespace Icinga\Forms\Security; namespace Icinga\Forms\Security;
use LogicException;
use Zend_Form_Element;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Exception\AlreadyExistsException; use Icinga\Application\Modules\Manager;
use Icinga\Exception\NotFoundError; use Icinga\Data\Filter\Filter;
use Icinga\Forms\ConfigForm; use Icinga\Forms\ConfigForm;
use Icinga\Forms\RepositoryForm;
use Icinga\Util\StringHelper; use Icinga\Util\StringHelper;
use Zend_Form_Element;
/** /**
* Form for managing roles * Form for managing roles
*/ */
class RoleForm extends ConfigForm class RoleForm extends RepositoryForm
{ {
/** /**
* Provided permissions by currently loaded modules * The name to use instead of `*`
*/
const WILDCARD_NAME = 'allAndEverything';
/**
* Provided permissions by currently installed modules
* *
* @var array * @var array
*/ */
protected $providedPermissions; protected $providedPermissions;
/** /**
* Provided restrictions by currently loaded modules * Provided restrictions by currently installed modules
* *
* @var array * @var array
*/ */
protected $providedRestrictions; protected $providedRestrictions;
/**
* {@inheritdoc}
*/
public function init() public function init()
{ {
$this->providedPermissions = array(
'*' => $this->translate('Allow everything') . ' (*)',
'application/share/navigation' => $this->translate('Allow to share navigation items')
. ' (application/share/navigation)',
'application/stacktraces' => $this->translate(
'Allow to adjust in the preferences whether to show stacktraces'
) . ' (application/stacktraces)',
'application/log' => $this->translate('Allow to view the application log')
. ' (application/log)',
'admin' => $this->translate(
'Grant admin permissions, e.g. manage announcements'
) . ' (admin)',
'config/*' => $this->translate('Allow config access') . ' (config/*)'
);
$helper = new Zend_Form_Element('bogus'); $helper = new Zend_Form_Element('bogus');
$this->providedRestrictions = array(
$helper->filterName('application/share/users') => array( $this->providedPermissions['application'] = [
'name' => 'application/share/users', $helper->filterName('application/share/navigation') => [
'description' => $this->translate( 'label' => $this->translate('Allow to share navigation items'),
'Restrict which users this role can share items and information with' 'name' => 'application/share/navigation'
) ],
), $helper->filterName('application/stacktraces') => [
$helper->filterName('application/share/groups') => array( 'label' => $this->translate('Allow to adjust in the preferences whether to show stacktraces'),
'name' => 'application/share/groups', 'name' => 'application/stacktraces'
'description' => $this->translate( ],
'Restrict which groups this role can share items and information with' $helper->filterName('application/log') => [
) 'label' => $this->translate('Allow to view the application log'),
) 'name' => 'application/log'
); ],
$helper->filterName('admin') => [
'label' => $this->translate('Grant admin permissions, e.g. manage announcements'),
'name' => 'admin'
],
$helper->filterName('config/*') => [
'label' => $this->translate('Allow config access'),
'name' => 'config/*'
]
];
$this->providedRestrictions['application'] = [
$helper->filterName('application/share/users') => [
'label' => $this->translate('Restrict which users this role can share items and information with'),
'name' => 'application/share/users'
],
$helper->filterName('application/share/groups') => [
'label' => $this->translate('Restrict which groups this role can share items and information with'),
'name' => 'application/share/groups'
]
];
$mm = Icinga::app()->getModuleManager(); $mm = Icinga::app()->getModuleManager();
foreach ($mm->listInstalledModules() as $moduleName) { foreach ($mm->listInstalledModules() as $moduleName) {
$modulePermission = $mm::MODULE_PERMISSION_NS . $moduleName; $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
$this->providedPermissions[$modulePermission] = sprintf( $this->providedPermissions[$moduleName][$helper->filterName($modulePermission)] = [
$this->translate('Allow access to module %s') . ' (%s)', 'label' => sprintf($this->translate('Allow access to module %s'), $moduleName),
$moduleName, 'name' => $modulePermission,
$modulePermission 'isUsagePerm' => true
); ];
$module = $mm->getModule($moduleName, false); $module = $mm->getModule($moduleName, false);
foreach ($module->getProvidedPermissions() as $permission) { $permissions = $module->getProvidedPermissions();
if (count($permissions) > 1) {
$this->providedPermissions[$moduleName][$helper->filterName($moduleName . '/*')] = [
'label' => $this->translate('Full Access'),
'name' => $moduleName . '/*',
'isFullPerm' => true
];
}
foreach ($permissions as $permission) {
/** @var object $permission */ /** @var object $permission */
$this->providedPermissions[$permission->name] = $permission->description $this->providedPermissions[$moduleName][$helper->filterName($permission->name)] = [
. ' (' . $permission->name . ')'; 'label' => $permission->description,
'name' => $permission->name
];
} }
foreach ($module->getProvidedRestrictions() as $restriction) { foreach ($module->getProvidedRestrictions() as $restriction) {
/** @var object $restriction */ $this->providedRestrictions[$moduleName][$helper->filterName($restriction->name)] = [
// Zend only permits alphanumerics, the underscore, the circumflex and any ASCII character in range 'label' => $restriction->description,
// \x7f to \xff (127 to 255) 'name' => $restriction->name
$name = $helper->filterName($restriction->name); ];
while (isset($this->providedRestrictions[$name])) { }
// Because Zend_Form_Element::filterName() replaces any not permitted character with the empty }
// string we may have duplicate names, e.g. 're/striction' and 'restriction' }
$name .= '_';
protected function createFilter()
{
return Filter::where('name', $this->getIdentifier());
}
public function createInsertElements(array $formData = array())
{
$this->addElement(
'text',
'name',
[
'required' => true,
'label' => $this->translate('Role Name'),
'description' => $this->translate('The name of the role')
]
);
$this->addElement(
'textarea',
'users',
[
'label' => $this->translate('Users'),
'description' => $this->translate('Comma-separated list of users that are assigned to the role')
]
);
$this->addElement(
'textarea',
'groups',
[
'label' => $this->translate('Groups'),
'description' => $this->translate('Comma-separated list of groups that are assigned to the role')
]
);
$this->addElement(
'checkbox',
self::WILDCARD_NAME,
[
'autosubmit' => true,
'label' => $this->translate('Administrative Access'),
'description' => $this->translate('Everything is allowed')
]
);
if (! isset($formData[self::WILDCARD_NAME]) || ! $formData[self::WILDCARD_NAME]) {
foreach ($this->providedPermissions as $moduleName => $permissionList) {
$this->sortPermissions($permissionList);
$elements = [];
$hasFullPerm = false;
foreach ($permissionList as $name => $spec) {
$elements[] = $name;
$this->addElement(
'checkbox',
$name,
[
'ignore' => isset($spec['isUsagePerm']) ? false : $hasFullPerm,
'autosubmit' => isset($spec['isFullPerm']),
'disabled' => $hasFullPerm ?: null,
'value' => $hasFullPerm,
'label' => $spec['name'],
'description' => $spec['label']
]
);
if (isset($spec['isFullPerm'])) {
$hasFullPerm = isset($formData[$name]) && $formData[$name];
}
} }
$this->providedRestrictions[$name] = array(
'description' => $restriction->description, if (isset($this->providedRestrictions[$moduleName])) {
'name' => $restriction->name foreach ($this->providedRestrictions[$moduleName] as $name => $spec) {
); $elements[] = $name;
$this->addElement(
'text',
$name,
[
'label' => $spec['name'],
'description' => $spec['label']
]
);
}
}
$this->addDisplayGroup($elements, $moduleName . '_elements', [
'legend' => $moduleName !== 'application'
? sprintf($this->translate('Module: %s'), $moduleName)
: 'Icinga Web 2',
'decorators' => [
'FormElements',
['Fieldset', ['class' => 'collapsible']]
]
]);
} }
}
}
/**
* {@inheritdoc}
*/
public function createElements(array $formData = array())
{
$this->addElements(array(
array(
'text',
'name',
array(
'required' => true,
'label' => $this->translate('Role Name'),
'description' => $this->translate('The name of the role'),
'ignore' => true
),
),
array(
'textarea',
'users',
array(
'label' => $this->translate('Users'),
'description' => $this->translate('Comma-separated list of users that are assigned to the role')
),
),
array(
'textarea',
'groups',
array(
'label' => $this->translate('Groups'),
'description' => $this->translate('Comma-separated list of groups that are assigned to the role')
),
),
array(
'multiselect',
'permissions',
array(
'label' => $this->translate('Permissions Set'),
'description' => $this->translate(
'The permissions to grant. You may select more than one permission'
),
'multiOptions' => $this->providedPermissions,
'class' => 'grant-permissions'
)
)
));
foreach ($this->providedRestrictions as $name => $spec) {
$this->addElement(
'text',
$name,
array(
'label' => $spec['name'],
'description' => $spec['description']
)
);
}
return $this;
}
/**
* Load a role
*
* @param string $name The name of the role
*
* @return $this
*
* @throws LogicException If the config is not set
* @throws NotFoundError If the given role does not exist
* @see ConfigForm::setConfig() For setting the config.
*/
public function load($name)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t load role \'%s\'. Config is not set', $name));
}
if (! $this->config->hasSection($name)) {
throw new NotFoundError(
$this->translate('Can\'t load role \'%s\'. Role does not exist'),
$name
);
}
$role = $this->config->getSection($name)->toArray();
$role['permissions'] = ! empty($role['permissions'])
? StringHelper::trimSplit($role['permissions'])
: null;
$role['name'] = $name;
$restrictions = array();
foreach ($this->providedRestrictions as $name => $spec) {
if (isset($role[$spec['name']])) {
// Translate restriction names to filtered element names
$restrictions[$name] = $role[$spec['name']];
unset($role[$spec['name']]);
}
}
$role = array_merge($role, $restrictions);
$this->populate($role);
return $this;
}
/**
* Add a role
*
* @param string $name The name of the role
* @param array $values
*
* @return $this
*
* @throws LogicException If the config is not set
* @throws AlreadyExistsException If the role to add already exists
* @see ConfigForm::setConfig() For setting the config.
*/
public function add($name, array $values)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t add role \'%s\'. Config is not set', $name));
}
if ($this->config->hasSection($name)) {
throw new AlreadyExistsException(
$this->translate('Can\'t add role \'%s\'. Role already exists'),
$name
);
}
$this->config->setSection($name, $values);
return $this;
}
/**
* Remove a role
*
* @param string $name The name of the role
*
* @return $this
*
* @throws LogicException If the config is not set
* @throws NotFoundError If the role does not exist
* @see ConfigForm::setConfig() For setting the config.
*/
public function remove($name)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t remove role \'%s\'. Config is not set', $name));
}
if (! $this->config->hasSection($name)) {
throw new NotFoundError(
$this->translate('Can\'t remove role \'%s\'. Role does not exist'),
$name
);
}
$this->config->removeSection($name);
return $this;
}
/**
* Update a role
*
* @param string $name The possibly new name of the role
* @param array $values
* @param string $oldName The name of the role to update
*
* @return $this
*
* @throws LogicException If the config is not set
* @throws NotFoundError If the role to update does not exist
* @see ConfigForm::setConfig() For setting the config.
*/
public function update($name, array $values, $oldName)
{
if (! isset($this->config)) {
throw new LogicException(sprintf('Can\'t update role \'%s\'. Config is not set', $name));
}
if ($name !== $oldName) {
// The permission got a new name
$this->remove($oldName);
$this->add($name, $values);
} else { } else {
if (! $this->config->hasSection($name)) { // Previously it was possible to define restrictions for super users, so make sure
throw new NotFoundError( // to not remove any restrictions which were set before the enforced separation
$this->translate('Can\'t update role \'%s\'. Role does not exist'), foreach ($this->providedRestrictions as $restrictionList) {
$name foreach ($restrictionList as $name => $_) {
); $this->addElement('hidden', $name);
}
} }
$this->config->setSection($name, $values);
} }
return $this;
} }
/** protected function createDeleteElements(array $formData)
* {@inheritdoc} {
*/ }
public function fetchEntry()
{
$role = parent::fetchEntry();
if ($role === false) {
return false;
}
$values = [
'name' => $role->name,
'users' => $role->users,
'groups' => $role->groups,
self::WILDCARD_NAME => $role->permissions === '*'
];
if (! empty($role->permissions) && $role->permissions !== '*') {
$permissions = StringHelper::trimSplit($role->permissions);
foreach ($this->providedPermissions as $moduleName => $permissionList) {
foreach ($permissionList as $name => $spec) {
if (in_array($spec['name'], $permissions, true)) {
$values[$name] = 1;
}
}
}
}
foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
foreach ($restrictionList as $name => $spec) {
if (isset($role->{$spec['name']})) {
$values[$name] = $role->{$spec['name']};
}
}
}
return (object) $values;
}
public function getValues($suppressArrayNotation = false) public function getValues($suppressArrayNotation = false)
{ {
$values = static::transformEmptyValuesToNull(parent::getValues($suppressArrayNotation)); $values = parent::getValues($suppressArrayNotation);
if (isset($values['permissions'])) {
$values['permissions'] = implode(', ', $values['permissions']); foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
} foreach ($restrictionList as $name => $spec) {
$restrictions = array(); if (isset($values[$name])) {
foreach ($this->providedRestrictions as $name => $spec) { $values[$spec['name']] = $values[$name];
if (isset($values[$name])) { unset($values[$name]);
// Translate filtered element names to restriction names }
$restrictions[$spec['name']] = $values[$name];
unset($values[$name]);
} }
} }
$values = array_merge($values, $restrictions);
return $values; $permissions = [];
if (isset($values[self::WILDCARD_NAME]) && $values[self::WILDCARD_NAME]) {
$permissions[] = '*';
} else {
foreach ($this->providedPermissions as $moduleName => $permissionList) {
foreach ($permissionList as $name => $spec) {
if (isset($values[$name]) && $values[$name]) {
$permissions[] = $spec['name'];
}
unset($values[$name]);
}
}
}
unset($values[self::WILDCARD_NAME]);
$values['permissions'] = join(',', $permissions);
return ConfigForm::transformEmptyValuesToNull($values);
}
protected function getInsertMessage($success)
{
return $success ? $this->translate('Role created') : $this->translate('Role creation failed');
}
protected function getUpdateMessage($success)
{
return $success ? $this->translate('Role updated') : $this->translate('Role update failed');
}
protected function getDeleteMessage($success)
{
return $success ? $this->translate('Role removed') : $this->translate('Role removal failed');
}
protected function sortPermissions(& $permissions)
{
return uasort($permissions, function ($a, $b) {
if (isset($a['isUsagePerm'])) {
return isset($b['isFullPerm']) ? 1 : -1;
} elseif (isset($b['isUsagePerm'])) {
return isset($a['isFullPerm']) ? -1 : 1;
}
$aParts = explode('/', $a['name']);
$bParts = explode('/', $b['name']);
do {
$a = array_shift($aParts);
$b = array_shift($bParts);
} while ($a === $b);
return strnatcmp($a, $b);
});
} }
} }