diff --git a/application/controllers/RoleController.php b/application/controllers/RoleController.php index eb3a0f2cd..246d8900b 100644 --- a/application/controllers/RoleController.php +++ b/application/controllers/RoleController.php @@ -4,12 +4,10 @@ namespace Icinga\Controllers; use Icinga\Application\Config; -use Icinga\Exception\AlreadyExistsException; +use Icinga\Authentication\RolesConfig; use Icinga\Exception\NotFoundError; -use Icinga\Forms\ConfirmRemovalForm; use Icinga\Forms\Security\RoleForm; use Icinga\Web\Controller\AuthBackendController; -use Icinga\Web\Notification; /** * Manage user permissions and restrictions based on roles @@ -45,28 +43,13 @@ class RoleController extends AuthBackendController public function addAction() { $this->assertPermission('config/authentication/roles/add'); - $role = new RoleForm(array( - 'onSuccess' => function (RoleForm $role) { - $name = $role->getElement('name')->getValue(); - $values = $role->getValues(); - try { - $role->add($name, $values); - } 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(); + + $role = new RoleForm(); + $role->setRedirectUrl('role/list'); + $role->setRepository(new RolesConfig()); + $role->setSubmitLabel($this->translate('Create Role')); + $role->add()->handleRequest(); + $this->renderForm($role, $this->translate('New Role')); } @@ -78,35 +61,20 @@ class RoleController extends AuthBackendController public function editAction() { $this->assertPermission('config/authentication/roles/edit'); + $name = $this->params->getRequired('role'); $role = new RoleForm(); + $role->setRedirectUrl('role/list'); + $role->setRepository(new RolesConfig()); $role->setSubmitLabel($this->translate('Update Role')); + $role->edit($name); + try { - $role - ->setIniConfig(Config::app('roles', true)) - ->load($name); + $role->handleRequest(); } 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')); } @@ -116,35 +84,21 @@ class RoleController extends AuthBackendController public function removeAction() { $this->assertPermission('config/authentication/roles/remove'); + $name = $this->params->getRequired('role'); $role = new RoleForm(); + $role->setRedirectUrl('role/list'); + $role->setRepository(new RolesConfig()); + $role->setSubmitLabel($this->translate('Remove Role')); + $role->remove($name); + try { - $role - ->setIniConfig(Config::app('roles', true)) - ->load($name); + $role->handleRequest(); } 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) { - 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')); + + $this->renderForm($role, $this->translate('Remove Role')); } /** diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index de3f98abe..829c78389 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -3,305 +3,321 @@ namespace Icinga\Forms\Security; -use LogicException; -use Zend_Form_Element; use Icinga\Application\Icinga; -use Icinga\Exception\AlreadyExistsException; -use Icinga\Exception\NotFoundError; +use Icinga\Application\Modules\Manager; +use Icinga\Data\Filter\Filter; use Icinga\Forms\ConfigForm; +use Icinga\Forms\RepositoryForm; use Icinga\Util\StringHelper; +use Zend_Form_Element; /** * 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 */ protected $providedPermissions; /** - * Provided restrictions by currently loaded modules + * Provided restrictions by currently installed modules * * @var array */ protected $providedRestrictions; - /** - * {@inheritdoc} - */ 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'); - $this->providedRestrictions = array( - $helper->filterName('application/share/users') => array( - 'name' => 'application/share/users', - 'description' => $this->translate( - 'Restrict which users this role can share items and information with' - ) - ), - $helper->filterName('application/share/groups') => array( - 'name' => 'application/share/groups', - 'description' => $this->translate( - 'Restrict which groups this role can share items and information with' - ) - ) - ); + + $this->providedPermissions['application'] = [ + $helper->filterName('application/share/navigation') => [ + 'label' => $this->translate('Allow to share navigation items'), + 'name' => 'application/share/navigation' + ], + $helper->filterName('application/stacktraces') => [ + 'label' => $this->translate('Allow to adjust in the preferences whether to show stacktraces'), + 'name' => 'application/stacktraces' + ], + $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(); foreach ($mm->listInstalledModules() as $moduleName) { - $modulePermission = $mm::MODULE_PERMISSION_NS . $moduleName; - $this->providedPermissions[$modulePermission] = sprintf( - $this->translate('Allow access to module %s') . ' (%s)', - $moduleName, - $modulePermission - ); + $modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName; + $this->providedPermissions[$moduleName][$helper->filterName($modulePermission)] = [ + 'label' => sprintf($this->translate('Allow access to module %s'), $moduleName), + 'name' => $modulePermission, + 'isUsagePerm' => true + ]; $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 */ - $this->providedPermissions[$permission->name] = $permission->description - . ' (' . $permission->name . ')'; + $this->providedPermissions[$moduleName][$helper->filterName($permission->name)] = [ + 'label' => $permission->description, + 'name' => $permission->name + ]; } + foreach ($module->getProvidedRestrictions() as $restriction) { - /** @var object $restriction */ - // Zend only permits alphanumerics, the underscore, the circumflex and any ASCII character in range - // \x7f to \xff (127 to 255) - $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 .= '_'; + $this->providedRestrictions[$moduleName][$helper->filterName($restriction->name)] = [ + 'label' => $restriction->description, + 'name' => $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, - 'name' => $restriction->name - ); + + if (isset($this->providedRestrictions[$moduleName])) { + 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 { - if (! $this->config->hasSection($name)) { - throw new NotFoundError( - $this->translate('Can\'t update role \'%s\'. Role does not exist'), - $name - ); + // Previously it was possible to define restrictions for super users, so make sure + // to not remove any restrictions which were set before the enforced separation + foreach ($this->providedRestrictions as $restrictionList) { + foreach ($restrictionList as $name => $_) { + $this->addElement('hidden', $name); + } } - $this->config->setSection($name, $values); } - return $this; } - /** - * {@inheritdoc} - */ + protected function createDeleteElements(array $formData) + { + } + + 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) { - $values = static::transformEmptyValuesToNull(parent::getValues($suppressArrayNotation)); - if (isset($values['permissions'])) { - $values['permissions'] = implode(', ', $values['permissions']); - } - $restrictions = array(); - foreach ($this->providedRestrictions as $name => $spec) { - if (isset($values[$name])) { - // Translate filtered element names to restriction names - $restrictions[$spec['name']] = $values[$name]; - unset($values[$name]); + $values = parent::getValues($suppressArrayNotation); + + foreach ($this->providedRestrictions as $moduleName => $restrictionList) { + foreach ($restrictionList as $name => $spec) { + if (isset($values[$name])) { + $values[$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); + }); } }