diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 8f5f9eb42..73d40261c 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -24,14 +24,17 @@ class ConfigController extends ActionController public function init() { $this->view->tabs = Widget::create('tabs')->add('index', array( - 'title' => 'Application', + 'title' => $this->translate('Application'), 'url' => 'config' ))->add('authentication', array( - 'title' => 'Authentication', + 'title' => $this->translate('Authentication'), 'url' => 'config/authentication' ))->add('resources', array( - 'title' => 'Resources', + 'title' => $this->translate('Resources'), 'url' => 'config/resource' + ))->add('permissions', array( + 'title' => $this->translate('Permissions'), + 'url' => 'permissions' )); } diff --git a/application/controllers/PermissionsController.php b/application/controllers/PermissionsController.php new file mode 100644 index 000000000..8709a28ad --- /dev/null +++ b/application/controllers/PermissionsController.php @@ -0,0 +1,150 @@ +view->tabs = Widget::create('tabs')->add('index', array( + 'title' => $this->translate('Application'), + 'url' => 'config' + ))->add('authentication', array( + 'title' => $this->translate('Authentication'), + 'url' => 'config/authentication' + ))->add('resources', array( + 'title' => $this->translate('Resources'), + 'url' => 'config/resource' + ))->add('permissions', array( + 'title' => $this->translate('Permissions'), + 'url' => 'permissions' + )); + } + + public function indexAction() + { + $this->view->tabs->activate('permissions'); + $this->view->roles = Config::app('roles', true); + } + + public function newAction() + { + $role = new RoleForm(array( + 'onSuccess' => function (RoleForm $role) { + $name = $role->getElement('name')->getValue(); + $values = $role->getValues(); + try { + $role->add($name, $values); + } catch (InvalidArgumentException $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('security') + ->handleRequest(); + $this->view->form = $role; + } + + public function updateAction() + { + $name = $this->_request->getParam('role'); + if (empty($name)) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('Required parameter \'%s\' missing'), 'role'), + 400 + ); + } + $role = new RoleForm(); + $role->setSubmitLabel($this->translate('Update Role')); + try { + $role + ->setIniConfig(Config::app('roles', true)) + ->load($name); + } catch (InvalidArgumentException $e) { + throw new Zend_Controller_Action_Exception( + $e->getMessage(), + 400 + ); + } + $role + ->setOnSuccess(function (RoleForm $role) use ($name) { + $oldName = $name; + $name = $role->getElement('name')->getValue(); + $values = $role->getValues(); + try { + $role->update($name, $values, $oldName); + } catch (InvalidArgumentException $e) { + $role->addError($e->getMessage()); + return false; + } + if ($role->save()) { + Notification::success(t('Role updated')); + return true; + } + return false; + }) + ->setRedirectUrl('security') + ->handleRequest(); + $this->view->name = $name; + $this->view->form = $role; + } + + public function removeAction() + { + $name = $this->_request->getParam('role'); + if (empty($name)) { + throw new Zend_Controller_Action_Exception( + sprintf($this->translate('Required parameter \'%s\' missing'), 'role'), + 400 + ); + } + $role = new RoleForm(); + try { + $role + ->setIniConfig(Config::app('roles', true)) + ->load($name); + } catch (InvalidArgumentException $e) { + throw new Zend_Controller_Action_Exception( + $e->getMessage(), + 400 + ); + } + $confirmation = new ConfirmRemovalForm(array( + 'onSuccess' => function (ConfirmRemovalForm $confirmation) use ($name, $role) { + try { + $role->remove($name); + } catch (InvalidArgumentException $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('security') + ->handleRequest(); + $this->view->name = $name; + $this->view->form = $confirmation; + } +} diff --git a/application/forms/ConfigForm.php b/application/forms/ConfigForm.php index fcf674cad..28d3111ea 100644 --- a/application/forms/ConfigForm.php +++ b/application/forms/ConfigForm.php @@ -5,6 +5,7 @@ namespace Icinga\Forms; use Exception; +use Zend_Form_Decorator_Abstract; use Icinga\Web\Form; use Icinga\Application\Config; use Icinga\File\Ini\IniWriter; @@ -58,7 +59,8 @@ class ConfigForm extends Form 'viewScript' => 'showConfiguration.phtml', 'errorMessage' => $e->getMessage(), 'configString' => $writer->render(), - 'filePath' => $this->config->getConfigFile() + 'filePath' => $this->config->getConfigFile(), + 'placement' => Zend_Form_Decorator_Abstract::PREPEND )); return false; } diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php new file mode 100644 index 000000000..f81cc9fd9 --- /dev/null +++ b/application/forms/Security/RoleForm.php @@ -0,0 +1,225 @@ +getModuleManager()->getLoadedModules() as $module) { + foreach ($module->getProvidedPermissions() as $permission) { + /** @var object $permission */ + $this->providedPermissions[$permission->name] = $permission->name . ': ' . $permission->description; + } + foreach ($module->getProvidedRestrictions() as $restriction) { + /** @var object $restriction */ + $this->providedRestrictions[$restriction->name] = $restriction->name . ': ' . $restriction->description; + } + } + } + + /** + * (non-PHPDoc) + * @see \Icinga\Web\Form::createElements() For the method documentation. + */ + public function createElements(array $formData = array()) + { + $this->addElements(array( + array( + 'text', + 'name', + array( + 'required' => true, + 'label' => t('Role Name'), + 'description' => t('The name of the role') + ), + ), + array( + 'textarea', + 'users', + array( + 'label' => t('Users'), + 'description' => t('Comma-separated list of users that are assigned to the role') + ), + ), + array( + 'textarea', + 'groups', + array( + 'label' => t('Groups'), + 'description' => t('Comma-separated list of groups that are assigned to the role') + ), + ), + array( + 'multiselect', + 'permissions', + array( + 'label' => t('Permissions Set'), + 'description' => t('The permissions to grant. You may select more than one permission'), + 'multiOptions' => $this->providedPermissions + ) + ) + )); + return $this; + } + + /** + * Load a role + * + * @param string $name The name of the role + * + * @return $this + * + * @throws LogicException If the config is not set + * @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 InvalidArgumentException(sprintf( + t('Can\'t load role \'%s\'. Role does not exist'), + $name + )); + } + $role = $this->config->getSection($name)->toArray(); + $role['permissions'] = ! empty($role['permissions']) + ? String::trimSplit($role['permissions']) + : null; + $role['name'] = $name; + $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 InvalidArgumentException 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 InvalidArgumentException(sprintf( + t('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 InvalidArgumentException 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 InvalidArgumentException(sprintf( + t('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 InvalidArgumentException 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 InvalidArgumentException(sprintf( + t('Can\'t update role \'%s\'. Role does not exist'), + $name + )); + } + $this->config->setSection($name, $values); + } + return $this; + } + + /** + * (non-PHPDoc) + * @see \Zend_Form::getValues() For the method documentation. + */ + public function getValues($suppressArrayNotation = false) + { + $permissions = $this->getElement('permissions')->getValue(); + return array( + 'users' => $this->getElement('users')->getValue(), + 'groups' => $this->getElement('groups')->getValue(), + 'permissions' => ! empty($permissions) ? implode(', ', $permissions) : null + ); + } +} diff --git a/application/views/scripts/permissions/index.phtml b/application/views/scripts/permissions/index.phtml new file mode 100644 index 000000000..285d2f894 --- /dev/null +++ b/application/views/scripts/permissions/index.phtml @@ -0,0 +1,45 @@ +
+ +
+
+
+

translate('Permissions') ?>

+ isEmpty()): ?> + translate('No permissions found.') ?> + + + + + + + + + + + + $role): /** @var object $role */ ?> + + + + + + + + + +
translate('Name') ?>translate('Permissions') ?>translate('Users') ?>translate('Groups') ?>
+ escape($name) ?> + + escape($role->permissions) ?>escape($role->users) ?>escape($role->groups) ?> + + translate('Remove role') ?> + +
+ + + translate('New Role') ?> + +
+
diff --git a/application/views/scripts/permissions/new.phtml b/application/views/scripts/permissions/new.phtml new file mode 100644 index 000000000..d5f9e7e33 --- /dev/null +++ b/application/views/scripts/permissions/new.phtml @@ -0,0 +1,4 @@ +
+

translate('New Role') ?>

+ +
diff --git a/application/views/scripts/permissions/remove.phtml b/application/views/scripts/permissions/remove.phtml new file mode 100644 index 000000000..a9360e78f --- /dev/null +++ b/application/views/scripts/permissions/remove.phtml @@ -0,0 +1,4 @@ +
+

translate('Remove Role %s'), $name) ?>

+ +
diff --git a/application/views/scripts/permissions/update.phtml b/application/views/scripts/permissions/update.phtml new file mode 100644 index 000000000..8cb235a59 --- /dev/null +++ b/application/views/scripts/permissions/update.phtml @@ -0,0 +1,4 @@ +
+

translate('Update Role %s'), $name) ?>

+ +
diff --git a/application/views/scripts/showConfiguration.phtml b/application/views/scripts/showConfiguration.phtml index 1dde3b16b..682b349c9 100644 --- a/application/views/scripts/showConfiguration.phtml +++ b/application/views/scripts/showConfiguration.phtml @@ -1,6 +1,5 @@

translate('Saving Configuration Failed'); ?>

-

translate('The file %s couldn\'t be stored. (Error: "%s")'), @@ -25,4 +24,4 @@

     escape($configString); ?>
   
-

\ No newline at end of file +

diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php index 78e505d06..6469d3929 100644 --- a/library/Icinga/Application/Modules/Manager.php +++ b/library/Icinga/Application/Modules/Manager.php @@ -404,10 +404,9 @@ class Manager } /** - * Return an array containing all loaded modules + * Get the currently loaded modules * - * @return array - * @see Module + * @return Module[] */ public function getLoadedModules() { diff --git a/library/Icinga/Authentication/AdmissionLoader.php b/library/Icinga/Authentication/AdmissionLoader.php index 65d99f427..d97fc534e 100644 --- a/library/Icinga/Authentication/AdmissionLoader.php +++ b/library/Icinga/Authentication/AdmissionLoader.php @@ -5,6 +5,7 @@ namespace Icinga\Authentication; use Icinga\Application\Config; +use Icinga\Application\Logger; use Icinga\Exception\NotReadableError; use Icinga\Data\ConfigObject; use Icinga\User; @@ -43,72 +44,46 @@ class AdmissionLoader } /** - * Get user permissions + * Get user permissions and restrictions * - * @param User $user + * @param User $user * * @return array */ - public function getPermissions(User $user) + public function getPermissionsAndRestrictions(User $user) { $permissions = array(); + $restrictions = array(); + $username = $user->getUsername(); try { - $config = Config::app('permissions'); + $roles = Config::app('roles'); } catch (NotReadableError $e) { Logger::error( - 'Can\'t get permissions for user \'%s\'. An exception was thrown:', - $user->getUsername(), + 'Can\'t get permissions and restrictions for user \'%s\'. An exception was thrown:', + $username, $e ); - return $permissions; + return array($permissions, $restrictions); } - $username = $user->getUsername(); $userGroups = $user->getGroups(); - foreach ($config as $section) { - if (! empty($section->permissions) - && $this->match($username, $userGroups, $section) - ) { + foreach ($roles as $role) { + if ($this->match($username, $userGroups, $role)) { $permissions = array_merge( $permissions, - array_diff(String::trimSplit($section->permissions), $permissions) + array_diff(String::trimSplit($role->permissions), $permissions) ); + $restrictionsFromRole = $role->toArray(); + unset($restrictionsFromRole['users']); + unset($restrictionsFromRole['groups']); + unset($restrictionsFromRole['permissions']); + foreach ($restrictionsFromRole as $name => $restriction) { + if (! isset($restrictions[$name])) { + $restrictions[$name] = array(); + } + $restrictions[$name][] = $restriction; + } } } - return $permissions; - } - - /** - * Get user restrictions - * - * @param User $user - * - * @return array - */ - public function getRestrictions(User $user) - { - $restrictions = array(); - try { - $config = Config::app('restrictions'); - } catch (NotReadableError $e) { - Logger::error( - 'Can\'t get restrictions for user \'%s\'. An exception was thrown:', - $user->getUsername(), - $e - ); - return $restrictions; - } - $username = $user->getUsername(); - $userGroups = $user->getGroups(); - foreach ($config as $section) { - if (! empty($section->restriction) - && $this->match($username, $userGroups, $section) - ) { - $restrictions = array_merge( - $restrictions, - array_diff(String::trimSplit($section->restriction), $restrictions) - ); - } - } - return $restrictions; + return array($permissions, $restrictions); } } diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index 88258351e..0a9b40981 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -107,8 +107,9 @@ class Manager } $user->setGroups($groups); $admissionLoader = new AdmissionLoader(); - $user->setPermissions($admissionLoader->getPermissions($user)); - $user->setRestrictions($admissionLoader->getRestrictions($user)); + list($permissions, $restrictions) = $admissionLoader->getPermissionsAndRestrictions($user); + $user->setPermissions($permissions); + $user->setRestrictions($restrictions); $this->user = $user; if ($persist) { $this->persistCurrentUser(); diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php index e3b757b78..94a20faa5 100644 --- a/library/Icinga/File/Ini/IniWriter.php +++ b/library/Icinga/File/Ini/IniWriter.php @@ -61,12 +61,14 @@ class IniWriter extends Zend_Config_Writer_FileAbstract { if (file_exists($this->_filename)) { $oldconfig = new Zend_Config_Ini($this->_filename); + $content = file_get_contents($this->_filename); } else { $oldconfig = new Zend_Config(array()); + $content = ''; } $newconfig = $this->_config; - $editor = new IniEditor(@file_get_contents($this->_filename), $this->options); + $editor = new IniEditor($content, $this->options); $this->diffConfigs($oldconfig, $newconfig, $editor); $this->updateSectionOrder($newconfig, $editor); return $editor->getText(); diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 863d4f1c1..163dba455 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -46,7 +46,7 @@ class Form extends Zend_Form /** * The callback to call instead of Form::onSuccess() * - * @var Callback + * @var callable */ protected $onSuccess; @@ -122,29 +122,11 @@ class Form extends Zend_Form ); /** - * Create a new form - * - * Accepts an additional option `onSuccess' which is a callback that is called instead of this - * form's method. It is called using the following signature: (Form $form). - * - * @see Zend_Form::__construct() - * - * @throws LogicException In case `onSuccess' is not callable + * (non-PHPDoc) + * @see \Zend_Form::construct() For the method documentation. */ public function __construct($options = null) { - if (is_array($options) && isset($options['onSuccess'])) { - $this->onSuccess = $options['onSuccess']; - unset($options['onSuccess']); - } elseif (isset($options->onSuccess)) { - $this->onSuccess = $options->onSuccess; - unset($options->onSuccess); - } - - if ($this->onSuccess !== null && false === is_callable($this->onSuccess)) { - throw new LogicException('The option `onSuccess\' is not callable'); - } - // Zend's plugin loader reverses the order of added prefix paths thus trying our paths first before trying // Zend paths $this->addPrefixPaths(array( diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php index 366067c09..d61e32858 100644 --- a/modules/monitoring/configuration.php +++ b/modules/monitoring/configuration.php @@ -2,14 +2,54 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -/* @var $this \Icinga\Application\Modules\Module */ +/** @var $this \Icinga\Application\Modules\Module */ + +$this->providePermission( + 'monitoring/command/*', + $this->translate('Allow all commands') +); +$this->providePermission( + 'monitoring/command/schedule*', + $this->translate('Allow all scheduling checks and downtimes') +); +$this->providePermission( + 'monitoring/command/schedule-check', + $this->translate('Allow scheduling host and service checks') +); +$this->providePermission( + 'monitoring/command/schedule-downtime', + $this->translate('Allow scheduling host and service downtimes') +); +$this->providePermission( + 'monitoring/command/acknowledge-problem', + $this->translate('Allow acknowledging host and service problems') +); +$this->providePermission( + 'monitoring/command/add-comment', + $this->translate('Allow commenting on hosts and services') +); +$this->providePermission( + 'monitoring/command/remove*', + $this->translate('Allow removing problem acknowledgements, host and service comments and downtimes') +); +$this->providePermission( + 'monitoring/command/remove-acknowledgement', + $this->translate('Allow removing problem acknowledgements') +); +$this->providePermission( + 'monitoring/command/remove-comment', + $this->translate('Allow removing host and service comments') +); +$this->providePermission( + 'monitoring/command/remove-downtime', + $this->translate('Allow removing host and service downtimes') +); + +$this->provideRestriction( + 'monitoring/filter', + $this->translate('Restrict views to the hosts and services that match the filter') +); -// TODO: We need to define a useful permission set for this module, the -// list provided here is just an example -$this->providePermission('commands/all', 'Allow to send all commands'); -$this->providePermission('commands/safe', 'Allow to to send a subset of "safe" commands'); -$this->providePermission('log', 'Allow full log access'); -$this->provideRestriction('filter', 'Filter accessible object'); $this->provideConfigTab('backends', array( 'title' => 'Backends', 'url' => 'config'