From aa567f8cd788ba8ad0a980321191e79e4f8a23f9 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 22 Jul 2015 14:56:02 +0200 Subject: [PATCH 01/32] Introduce ModuleActionController::$requiresModulePermission refs #9644 --- .../Web/Controller/ModuleActionController.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/library/Icinga/Web/Controller/ModuleActionController.php b/library/Icinga/Web/Controller/ModuleActionController.php index 9286ff679..d4cbda86d 100644 --- a/library/Icinga/Web/Controller/ModuleActionController.php +++ b/library/Icinga/Web/Controller/ModuleActionController.php @@ -24,6 +24,17 @@ class ModuleActionController extends ActionController */ protected $moduleName; + /** + * Whether the module permission is required to access to module + * + * Note that module permissions do not have any effect if the controller does not require authentication. + * + * @var bool + * + * @see $requiresAuthentication For enabling/disabling whether the controller requires authentication. + */ + protected $requiresModulePermission = true; + /** * (non-PHPDoc) * @see \Icinga\Web\Controller\ActionController For the method documentation. @@ -43,6 +54,29 @@ class ModuleActionController extends ActionController { } + /** + * Get whether the module permission is required to access to module + * + * @return bool + */ + public function getRequiresModulePermission() + { + return $this->requiresModulePermission; + } + + /** + * Set whether the module permission is required to access to module + * + * @param bool $required + * + * @return $this + */ + public function setRequiresModulePermission($required) + { + $this->requiresModulePermission = (bool) $required; + return $this; + } + public function Config($file = null) { if ($file === null) { From 5dcc307e878e41132245cd1cd87d1671f461970c Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 22 Jul 2015 14:59:35 +0200 Subject: [PATCH 02/32] Add namespace for module permissions to the module manager refs #9644 --- library/Icinga/Application/Modules/Manager.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php index e3e5ec6f2..9de86db18 100644 --- a/library/Icinga/Application/Modules/Manager.php +++ b/library/Icinga/Application/Modules/Manager.php @@ -24,6 +24,13 @@ use Icinga\Exception\NotReadableError; */ class Manager { + /** + * Namespace for module permissions + * + * @var string + */ + const MODULE_PERMISSION_NS = 'module/'; + /** * Array of all installed module's base directories * From bf590ed3801e659b4dff6d38db7026b048f1990c Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 22 Jul 2015 15:01:54 +0200 Subject: [PATCH 03/32] Provide module permissions automatically refs #9644 --- application/forms/Security/RoleForm.php | 46 +++++++++++++++---------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 114ddfc1f..970a0cc01 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -63,25 +63,35 @@ class RoleForm extends ConfigForm public function init() { $helper = new Zend_Form_Element('bogus'); - foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { - foreach ($module->getProvidedPermissions() as $permission) { - /** @var object $permission */ - $this->providedPermissions[$permission->name] = $permission->description . ' (' . $permission->name . ')'; - } - foreach ($module->getProvidedRestrictions() as $restriction) { - /** @var object $restriction */ - $name = $helper->filterName($restriction->name); // Zend only permits alphanumerics, the underscore, - // the circumflex and any ASCII character in range - // \x7f to \xff (127 to 255) - 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 .= '_'; + $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 + ); + if ($mm->hasEnabled($moduleName)) { + $module = $mm->getModule($moduleName); + foreach ($module->getProvidedPermissions() as $permission) { + /** @var object $permission */ + $this->providedPermissions[$permission->name] = $permission->description . ' (' . $permission->name . ')'; + } + foreach ($module->getProvidedRestrictions() as $restriction) { + /** @var object $restriction */ + $name = $helper->filterName($restriction->name); // Zend only permits alphanumerics, the underscore, + // the circumflex and any ASCII character in range + // \x7f to \xff (127 to 255) + 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[$name] = array( + 'description' => $restriction->description, + 'name' => $restriction->name + ); } - $this->providedRestrictions[$name] = array( - 'description' => $restriction->description, - 'name' => $restriction->name - ); } } } From b000ae3a37f959ae9937f4ad8d8237753df649a6 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 12:50:02 +0200 Subject: [PATCH 04/32] Do not require permissions if authentication is not required refs #9644 --- library/Icinga/Web/Controller/ActionController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php index 571f0792f..dc7a7e21e 100644 --- a/library/Icinga/Web/Controller/ActionController.php +++ b/library/Icinga/Web/Controller/ActionController.php @@ -152,7 +152,7 @@ class ActionController extends Zend_Controller_Action */ public function assertPermission($permission) { - if (! $this->Auth()->hasPermission($permission)) { + if ($this->requiresAuthentication && ! $this->Auth()->hasPermission($permission)) { throw new SecurityException('No permission for %s', $permission); } } From 7c5b4de087303693a95553af110dd1efdc207508 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 12:50:42 +0200 Subject: [PATCH 05/32] Fix coding style in RoleForm.php --- application/forms/Security/RoleForm.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 970a0cc01..19307262d 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -75,13 +75,14 @@ class RoleForm extends ConfigForm $module = $mm->getModule($moduleName); foreach ($module->getProvidedPermissions() as $permission) { /** @var object $permission */ - $this->providedPermissions[$permission->name] = $permission->description . ' (' . $permission->name . ')'; + $this->providedPermissions[$permission->name] = $permission->description + . ' (' . $permission->name . ')'; } foreach ($module->getProvidedRestrictions() as $restriction) { /** @var object $restriction */ - $name = $helper->filterName($restriction->name); // Zend only permits alphanumerics, the underscore, - // the circumflex and any ASCII character in range + // 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' From b4f4ce4406f8603d7e2e489a9c7b4514e70b4fa9 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 12:51:10 +0200 Subject: [PATCH 06/32] Require the module's permission for every module automatically Module permissions are not required if authentication is disabled on the controller. refs #9644 --- .../Web/Controller/ModuleActionController.php | 36 ++----------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/library/Icinga/Web/Controller/ModuleActionController.php b/library/Icinga/Web/Controller/ModuleActionController.php index d4cbda86d..b6d2274eb 100644 --- a/library/Icinga/Web/Controller/ModuleActionController.php +++ b/library/Icinga/Web/Controller/ModuleActionController.php @@ -5,6 +5,7 @@ namespace Icinga\Web\Controller; use Icinga\Application\Config; use Icinga\Application\Icinga; +use Icinga\Application\Modules\Manager; /** * Base class for module action controllers @@ -24,17 +25,6 @@ class ModuleActionController extends ActionController */ protected $moduleName; - /** - * Whether the module permission is required to access to module - * - * Note that module permissions do not have any effect if the controller does not require authentication. - * - * @var bool - * - * @see $requiresAuthentication For enabling/disabling whether the controller requires authentication. - */ - protected $requiresModulePermission = true; - /** * (non-PHPDoc) * @see \Icinga\Web\Controller\ActionController For the method documentation. @@ -45,6 +35,7 @@ class ModuleActionController extends ActionController $this->_helper->layout()->moduleName = $this->moduleName; $this->view->translationDomain = $this->moduleName; $this->moduleInit(); + $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->moduleName); } /** @@ -54,29 +45,6 @@ class ModuleActionController extends ActionController { } - /** - * Get whether the module permission is required to access to module - * - * @return bool - */ - public function getRequiresModulePermission() - { - return $this->requiresModulePermission; - } - - /** - * Set whether the module permission is required to access to module - * - * @param bool $required - * - * @return $this - */ - public function setRequiresModulePermission($required) - { - $this->requiresModulePermission = (bool) $required; - return $this; - } - public function Config($file = null) { if ($file === null) { From e8e01257e6dc1409fcedaef2c17c34e0733becce Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 12:53:08 +0200 Subject: [PATCH 07/32] Change interface of SearchDashboard to allow setting a user before calling ::search() Because search dashlets are provided by modules, the authenticated user's module permissions have to be validated before loading search dashlets provided by modules. refs #9644 --- library/Icinga/Web/Widget/SearchDashboard.php | 75 +++++++++---------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/library/Icinga/Web/Widget/SearchDashboard.php b/library/Icinga/Web/Widget/SearchDashboard.php index c1f234d2a..c394e32fa 100644 --- a/library/Icinga/Web/Widget/SearchDashboard.php +++ b/library/Icinga/Web/Widget/SearchDashboard.php @@ -12,6 +12,11 @@ use Icinga\Web\Url; */ class SearchDashboard extends Dashboard { + /** + * Name for the search pane + * + * @var string + */ const SEARCH_PANE = 'search'; /** @@ -19,13 +24,39 @@ class SearchDashboard extends Dashboard * * @param string $searchString * - * @return Dashboard|SearchDashboard + * @return $this */ - public static function search($searchString = '') + public function search($searchString = '') { - $dashboard = new static('searchDashboard'); - $dashboard->loadSearchDashlets($searchString); - return $dashboard; + $pane = $this->createPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search')); + $this->activate(self::SEARCH_PANE); + + $manager = Icinga::app()->getModuleManager(); + $searchUrls = array(); + + foreach ($manager->getLoadedModules() as $module) { + if ($this->getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) { + $moduleSearchUrls = $module->getSearchUrls(); + if (! empty($moduleSearchUrls)) { + if ($searchString === '') { + $pane->add(t('Ready to search'), 'search/hint'); + return $this; + } + $searchUrls = array_merge($searchUrls, $moduleSearchUrls); + } + } + } + + usort($searchUrls, array($this, 'compareSearchUrls')); + + foreach (array_reverse($searchUrls) as $searchUrl) { + $pane->addDashlet( + $searchUrl->title . ': ' . $searchString, + Url::fromPath($searchUrl->url, array('q' => $searchString)) + ); + } + + return $this; } /** @@ -43,40 +74,6 @@ class SearchDashboard extends Dashboard return parent::render(); } - /** - * Loads search dashlets - * - * @param string $searchString - */ - protected function loadSearchDashlets($searchString) - { - $pane = $this->createPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search')); - $this->activate(self::SEARCH_PANE); - - $manager = Icinga::app()->getModuleManager(); - $searchUrls = array(); - - foreach ($manager->getLoadedModules() as $module) { - $moduleSearchUrls = $module->getSearchUrls(); - if (! empty($moduleSearchUrls)) { - if ($searchString === '') { - $pane->add(t('Ready to search'), 'search/hint'); - return; - } - $searchUrls = array_merge($searchUrls, $moduleSearchUrls); - } - } - - usort($searchUrls, array($this, 'compareSearchUrls')); - - foreach (array_reverse($searchUrls) as $searchUrl) { - $pane->addDashlet( - $searchUrl->title . ': ' . $searchString, - Url::fromPath($searchUrl->url, array('q' => $searchString)) - ); - } - } - /** * Compare search URLs based on their priority * From fd77c3083804e1967d5830435dbcd0d0f5b491c2 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 12:57:23 +0200 Subject: [PATCH 08/32] Don't load module dashboards if the authenticated user lacks the module permission refs #9644 --- library/Icinga/Web/Widget/Dashboard.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/library/Icinga/Web/Widget/Dashboard.php b/library/Icinga/Web/Widget/Dashboard.php index 1d6a882f1..0bb7ce2e5 100644 --- a/library/Icinga/Web/Widget/Dashboard.php +++ b/library/Icinga/Web/Widget/Dashboard.php @@ -70,13 +70,13 @@ class Dashboard extends AbstractWidget { $manager = Icinga::app()->getModuleManager(); foreach ($manager->getLoadedModules() as $module) { - /** @var $module \Icinga\Application\Modules\Module */ - $this->mergePanes($module->getPaneItems()); + if ($this->getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) { + $this->mergePanes($module->getPaneItems()); + } } - if ($this->user !== null) { - $this->loadUserDashboards(); - } + + $this->loadUserDashboards(); return $this; } @@ -90,11 +90,11 @@ class Dashboard extends AbstractWidget { $output = array(); foreach ($this->panes as $pane) { - if ($pane->isUserWidget() === true) { + if ($pane->isUserWidget()) { $output[$pane->getName()] = $pane->toArray(); } foreach ($pane->getDashlets() as $dashlet) { - if ($dashlet->isUserWidget() === true) { + if ($dashlet->isUserWidget()) { $output[$pane->getName() . '.' . $dashlet->getTitle()] = $dashlet->toArray(); } } From cccb34e98c88d657305ad324e5e012f1d8e80893 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 12:58:53 +0200 Subject: [PATCH 09/32] Pass authenticated user to the SearchDashboard refs #9644 --- application/controllers/SearchController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/controllers/SearchController.php b/application/controllers/SearchController.php index 3816eb3e8..aa5cfaf9d 100644 --- a/application/controllers/SearchController.php +++ b/application/controllers/SearchController.php @@ -12,7 +12,9 @@ class SearchController extends ActionController { public function indexAction() { - $this->view->dashboard = SearchDashboard::search($this->params->get('q')); + $searchDashboard = new SearchDashboard(); + $searchDashboard->setUser($this->Auth()->getUser()); + $this->view->dashboard = $searchDashboard->search($this->params->get('q')); // NOTE: This renders the dashboard twice. Remove this once we can catch exceptions thrown in view scripts. $this->view->dashboard->render(); From a34ad0227f85c988f04b37c113b9715c07db969c Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 12:59:30 +0200 Subject: [PATCH 10/32] Don't load module menu entries if the authenticated user lacks the module permission refs #9644 --- library/Icinga/Web/Menu.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 87be9ec40..9b62afbdd 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -206,13 +206,14 @@ class Menu implements RecursiveIterator */ public static function load() { - /** @var $menu \Icinga\Web\Menu */ $menu = new static('menu'); $menu->addMainMenuItems(); + $auth = Manager::getInstance(); $manager = Icinga::app()->getModuleManager(); foreach ($manager->getLoadedModules() as $module) { - /** @var $module \Icinga\Application\Modules\Module */ - $menu->mergeSubMenus($module->getMenuItems()); + if ($auth->hasPermission($manager::MODULE_PERMISSION_NS . $module->getName())) { + $menu->mergeSubMenus($module->getMenuItems()); + } } return $menu->order(); } From b38b3b35b70c06f86325e0e5a61e3561c47c0e0e Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 13:00:23 +0200 Subject: [PATCH 11/32] Provide a test user in the DashboardTest refs #9644 --- test/php/library/Icinga/Web/Widget/DashboardTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/php/library/Icinga/Web/Widget/DashboardTest.php b/test/php/library/Icinga/Web/Widget/DashboardTest.php index 09b25adc4..181dec0a9 100644 --- a/test/php/library/Icinga/Web/Widget/DashboardTest.php +++ b/test/php/library/Icinga/Web/Widget/DashboardTest.php @@ -8,11 +8,11 @@ namespace Tests\Icinga\Web; require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php'); use Mockery; -use Icinga\Application\Icinga; +use Icinga\Test\BaseTestCase; +use Icinga\User; use Icinga\Web\Widget\Dashboard; use Icinga\Web\Widget\Dashboard\Pane; use Icinga\Web\Widget\Dashboard\Dashlet; -use Icinga\Test\BaseTestCase; class DashletWithMockedView extends Dashlet { @@ -52,6 +52,7 @@ class DashboardTest extends BaseTestCase $moduleMock->shouldReceive('getPaneItems')->andReturn(array( 'test-pane' => new Pane('Test Pane') )); + $moduleMock->shouldReceive('getName')->andReturn('test'); $moduleManagerMock = Mockery::mock('Icinga\Application\Modules\Manager'); $moduleManagerMock->shouldReceive('getLoadedModules')->andReturn(array( @@ -130,7 +131,10 @@ class DashboardTest extends BaseTestCase */ public function testLoadPaneItemsProvidedByEnabledModules() { + $user = new User('test'); + $user->setPermissions(array('*' => '*')); $dashboard = new Dashboard(); + $dashboard->setUser($user); $dashboard->load(); $this->assertCount( From 75147d6c626de83e7747a83d823f7a55fbcfe900 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 23 Jul 2015 13:01:05 +0200 Subject: [PATCH 12/32] Provide a test user in the SearchDashboardTest refs #9644 --- .../Icinga/Web/Widget/SearchDashboardTest.php | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/test/php/library/Icinga/Web/Widget/SearchDashboardTest.php b/test/php/library/Icinga/Web/Widget/SearchDashboardTest.php index 2db7be54f..6f5063946 100644 --- a/test/php/library/Icinga/Web/Widget/SearchDashboardTest.php +++ b/test/php/library/Icinga/Web/Widget/SearchDashboardTest.php @@ -5,6 +5,7 @@ namespace Tests\Icinga\Web; use Mockery; use Icinga\Test\BaseTestCase; +use Icinga\User; use Icinga\Web\Widget\SearchDashboard; class SearchDashboardTest extends BaseTestCase @@ -19,6 +20,7 @@ class SearchDashboardTest extends BaseTestCase $moduleMock->shouldReceive('getSearchUrls')->andReturn(array( $searchUrl )); + $moduleMock->shouldReceive('getName')->andReturn('test'); $moduleManagerMock = Mockery::mock('Icinga\Application\Modules\Manager'); $moduleManagerMock->shouldReceive('getLoadedModules')->andReturn(array( @@ -34,14 +36,22 @@ class SearchDashboardTest extends BaseTestCase */ public function testWhetherRenderThrowsAnExceptionWhenHasNoDashlets() { - $dashboard = SearchDashboard::search('pending'); + $user = new User('test'); + $user->setPermissions(array('*' => '*')); + $dashboard = new SearchDashboard(); + $dashboard->setUser($user); + $dashboard = $dashboard->search('pending'); $dashboard->getPane('search')->removeDashlets(); $dashboard->render(); } public function testWhetherSearchLoadsSearchDashletsFromModules() { - $dashboard = SearchDashboard::search('pending'); + $user = new User('test'); + $user->setPermissions(array('*' => '*')); + $dashboard = new SearchDashboard(); + $dashboard->setUser($user); + $dashboard = $dashboard->search('pending'); $result = $dashboard->getPane('search')->hasDashlet('Hosts: pending'); @@ -50,7 +60,11 @@ class SearchDashboardTest extends BaseTestCase public function testWhetherSearchProvidesHintWhenSearchStringIsEmpty() { - $dashboard = SearchDashboard::search(); + $user = new User('test'); + $user->setPermissions(array('*' => '*')); + $dashboard = new SearchDashboard(); + $dashboard->setUser($user); + $dashboard = $dashboard->search(); $result = $dashboard->getPane('search')->hasDashlet('Ready to search'); From 8d7f32cac1c1ea9cf6874aaf6ee1dfcca467079f Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 13:34:11 +0200 Subject: [PATCH 13/32] Fix static call of SearchDashboard::search() refs #9644 --- application/views/scripts/layout/menu.phtml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml index fe931c9d4..ee2ab0081 100644 --- a/application/views/scripts/layout/menu.phtml +++ b/application/views/scripts/layout/menu.phtml @@ -1,8 +1,11 @@ -getPane('search')->hasDashlets()): ?> +use Icinga\Web\Widget\SearchDashboard; + +$searchDashboard = new SearchDashboard(); +$searchDashboard->setUser($this->Auth()->getUser()); + +if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>

- \ No newline at end of file + From e1357c13868aa79993b3ebb193a36f06262dbb79 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 13:34:47 +0200 Subject: [PATCH 14/32] Require config/modules permission for listing and showing modules refs #9644 --- application/controllers/ConfigController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 206d3c88b..b9b79d446 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -93,6 +93,7 @@ class ConfigController extends Controller */ public function modulesAction() { + $this->assertPermission('config/modules'); // Overwrite tabs created in init // @TODO(el): This seems not natural to me. Module configuration should have its own controller. $this->view->tabs = Widget::create('tabs') @@ -118,6 +119,7 @@ class ConfigController extends Controller public function moduleAction() { + $this->assertPermission('config/modules'); $app = Icinga::app(); $manager = $app->getModuleManager(); $name = $this->getParam('name'); From ec1ddd82b3b152a1c41325f359b27d5b7f92a5c6 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 14:19:17 +0200 Subject: [PATCH 15/32] Move Module::__construct to the top of the function list refs #9664 --- library/Icinga/Application/Modules/Module.php | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 54e23d02c..43862dc99 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -102,7 +102,7 @@ class Module /** * Module metadata (version...) * - * @var stdClass + * @var object */ private $metadata; @@ -199,6 +199,30 @@ class Module */ protected $userGroupBackends = array(); + /** + * Create a new module object + * + * @param ApplicationBootstrap $app + * @param string $name + * @param string $basedir + */ + public function __construct(ApplicationBootstrap $app, $name, $basedir) + { + $this->app = $app; + $this->name = $name; + $this->basedir = $basedir; + $this->cssdir = $basedir . '/public/css'; + $this->jsdir = $basedir . '/public/js'; + $this->libdir = $basedir . '/library'; + $this->configdir = $app->getConfigDir('modules/' . $name); + $this->localedir = $basedir . '/application/locale'; + $this->formdir = $basedir . '/application/forms'; + $this->controllerdir = $basedir . '/application/controllers'; + $this->runScript = $basedir . '/run.php'; + $this->configScript = $basedir . '/configuration.php'; + $this->metadataFile = $basedir . '/module.info'; + } + /** * Provide a search URL * @@ -280,30 +304,6 @@ class Module return $this->menuItems[$name]; } - /** - * Create a new module object - * - * @param ApplicationBootstrap $app - * @param string $name - * @param string $basedir - */ - public function __construct(ApplicationBootstrap $app, $name, $basedir) - { - $this->app = $app; - $this->name = $name; - $this->basedir = $basedir; - $this->cssdir = $basedir . '/public/css'; - $this->jsdir = $basedir . '/public/js'; - $this->libdir = $basedir . '/library'; - $this->configdir = $app->getConfigDir('modules/' . $name); - $this->localedir = $basedir . '/application/locale'; - $this->formdir = $basedir . '/application/forms'; - $this->controllerdir = $basedir . '/application/controllers'; - $this->runScript = $basedir . '/run.php'; - $this->configScript = $basedir . '/configuration.php'; - $this->metadataFile = $basedir . '/module.info'; - } - /** * Register module * From 444fdadf13c0bded00b6eb82a7f10cbbdf4942af Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 14:23:48 +0200 Subject: [PATCH 16/32] Allow to get unloaded modules via Manager::getModule() refs #9644 --- .../Icinga/Application/Modules/Manager.php | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php index 9de86db18..8a3970841 100644 --- a/library/Icinga/Application/Modules/Manager.php +++ b/library/Icinga/Application/Modules/Manager.php @@ -408,22 +408,25 @@ class Manager } /** - * Return the module instance of the given module when it is loaded + * Get a module * - * @param string $name The module name to return + * @param string $name Name of the module + * @param bool $assertLoaded Whether or not to throw an exception if the module hasn't been loaded * * @return Module - * @throws ProgrammingError When the module hasn't been loaded + * @throws ProgrammingError If the module hasn't been loaded */ - public function getModule($name) + public function getModule($name, $assertLoaded = true) { - if (!$this->hasLoaded($name)) { - throw new ProgrammingError( - 'Cannot access module %s as it hasn\'t been loaded', - $name - ); + if ($this->hasLoaded($name)) { + return $this->loadedModules[$name]; + } elseif (! (bool) $assertLoaded) { + return new Module($this->app, $name, $this->getModuleDir($name)); } - return $this->loadedModules[$name]; + throw new ProgrammingError( + 'Can\'t access module %s because it hasn\'t been loaded', + $name + ); } /** From 519d025de5569ecc6e398b8a2f28660a400cd83b Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 14:24:48 +0200 Subject: [PATCH 17/32] Allow to get permissions and restrictions from unloaded modules refs #9644 --- library/Icinga/Application/Modules/Module.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 43862dc99..d96e6e962 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -113,6 +113,13 @@ class Module */ private $triedToLaunchConfigScript = false; + /** + * Whether the module's namespace has been registered on our autoloader + * + * @var bool + */ + protected $registeredAutoloader = false; + /** * Whether this module has been registered * @@ -855,6 +862,10 @@ class Module */ protected function registerAutoloader() { + if ($this->registeredAutoloader) { + return $this; + } + $moduleName = ucfirst($this->getName()); $moduleLibraryDir = $this->getLibDir(). '/'. $moduleName; @@ -867,6 +878,8 @@ class Module $this->app->getLoader()->registerNamespace('Icinga\\Module\\' . $moduleName. '\\Forms', $moduleFormDir); } + $this->registeredAutoloader = true; + return $this; } @@ -1011,7 +1024,7 @@ class Module */ protected function launchConfigScript() { - if ($this->triedToLaunchConfigScript || !$this->registered) { + if ($this->triedToLaunchConfigScript) { return; } $this->triedToLaunchConfigScript = true; @@ -1019,6 +1032,7 @@ class Module || ! is_readable($this->configScript)) { return; } + $this->registerAutoloader(); include($this->configScript); } From 149a974a448c34b0d795d91327e9fcca14920049 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 15:06:25 +0200 Subject: [PATCH 18/32] Fix coding style in Module.php refs #9644 --- library/Icinga/Application/Modules/Module.php | 231 +++++++++--------- 1 file changed, 113 insertions(+), 118 deletions(-) diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index d96e6e962..ad1ec1f5d 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -114,7 +114,7 @@ class Module private $triedToLaunchConfigScript = false; /** - * Whether the module's namespace has been registered on our autoloader + * Whether the module's namespaces have been registered on our autoloader * * @var bool */ @@ -233,25 +233,27 @@ class Module /** * Provide a search URL * - * @param string $title - * @param string $url - * @param int $priority + * @param string $title + * @param string $url + * @param int $priority + * + * @return $this */ public function provideSearchUrl($title, $url, $priority = 0) { - $searchUrl = (object) array( + $this->searchUrls[] = (object) array( 'title' => (string) $title, 'url' => (string) $url, 'priority' => (int) $priority ); - $this->searchUrls[] = $searchUrl; + return $this; } /** - * Return this module's search urls + * Get this module's search urls * - * @return array + * @return array */ public function getSearchUrls() { @@ -260,7 +262,7 @@ class Module } /** - * Get all Menu Items + * Get all pane items * * @return array */ @@ -273,8 +275,9 @@ class Module /** * Add a pane to dashboard * - * @param $name - * @return Pane + * @param string $name + * + * @return Pane */ protected function dashboard($name) { @@ -283,7 +286,7 @@ class Module } /** - * Get all Menu Items + * Get all menu items * * @return array */ @@ -294,11 +297,12 @@ class Module } /** - * Add a menu Section to the Sidebar menu + * Add or get a menu section * - * @param $name - * @param array $properties - * @return mixed + * @param string $name + * @param array $properties + * + * @return Menu */ protected function menuSection($name, array $properties = array()) { @@ -334,16 +338,16 @@ class Module ); return false; } - $this->registerWebIntegration(); $this->registered = true; + return true; } /** - * Return whether this module has been registered + * Get whether this module has been registered * - * @return bool + * @return bool */ public function isRegistered() { @@ -353,9 +357,9 @@ class Module /** * Test for an enabled module by name * - * @param string $name + * @param string $name * - * @return boolean + * @return bool */ public static function exists($name) { @@ -363,7 +367,7 @@ class Module } /** - * Get module by name + * Get a module by name * * @param string $name * @param bool $autoload @@ -425,7 +429,7 @@ class Module } /** - * Getter for module name + * Get the module name * * @return string */ @@ -435,7 +439,7 @@ class Module } /** - * Getter for module version + * Get the module version * * @return string */ @@ -445,7 +449,7 @@ class Module } /** - * Get module description + * Get the module description * * @return string */ @@ -455,7 +459,7 @@ class Module } /** - * Get module title (short description) + * Get the module title (short description) * * @return string */ @@ -465,9 +469,9 @@ class Module } /** - * Getter for module version + * Get the module dependencies * - * @return Array + * @return array */ public function getDependencies() { @@ -562,7 +566,7 @@ class Module } /** - * Getter for css file name + * Get the module's CSS directory * * @return string */ @@ -572,17 +576,7 @@ class Module } /** - * Getter for base directory - * - * @return string - */ - public function getBaseDir() - { - return $this->basedir; - } - - /** - * Get the controller directory + * Get the module's controller directory * * @return string */ @@ -592,7 +586,17 @@ class Module } /** - * Getter for library directory + * Get the module's base directory + * + * @return string + */ + public function getBaseDir() + { + return $this->basedir; + } + + /** + * Get the module's library directory * * @return string */ @@ -602,7 +606,7 @@ class Module } /** - * Getter for configuration directory + * Get the module's configuration directory * * @return string */ @@ -612,7 +616,7 @@ class Module } /** - * Getter for form directory + * Get the module's form directory * * @return string */ @@ -622,11 +626,11 @@ class Module } /** - * Getter for module config object + * Get the module config * - * @param string $file + * @param string $file * - * @return Config + * @return Config */ public function getConfig($file = 'config') { @@ -634,9 +638,7 @@ class Module } /** - * Retrieve provided permissions - * - * @param string $name Permission name + * Get provided permissions * * @return array */ @@ -647,9 +649,8 @@ class Module } /** - * Retrieve provided restrictions + * Get provided restrictions * - * @param string $name Restriction name * @return array */ public function getProvidedRestrictions() @@ -659,24 +660,11 @@ class Module } /** - * Whether the given permission name is supported + * Whether the module provides the given restriction * - * @param string $name Permission name + * @param string $name Restriction name * - * @return bool - */ - public function providesPermission($name) - { - $this->launchConfigScript(); - return array_key_exists($name, $this->permissionList); - } - - /** - * Whether the given restriction name is supported - * - * @param string $name Restriction name - * - * @return bool + * @return bool */ public function providesRestriction($name) { @@ -685,9 +673,22 @@ class Module } /** - * Retrieve this modules configuration tabs + * Whether the module provides the given permission * - * @return Icinga\Web\Widget\Tabs + * @param string $name Permission name + * + * @return bool + */ + public function providesPermission($name) + { + $this->launchConfigScript(); + return array_key_exists($name, $this->permissionList); + } + + /** + * Get the module configuration tabs + * + * @return \Icinga\Web\Widget\Tabs */ public function getConfigTabs() { @@ -705,9 +706,9 @@ class Module } /** - * Whether this module provides a setup wizard + * Whether the module provides a setup wizard * - * @return bool + * @return bool */ public function providesSetupWizard() { @@ -721,9 +722,9 @@ class Module } /** - * Return this module's setup wizard + * Get the module's setup wizard * - * @return SetupWizard + * @return SetupWizard */ public function getSetupWizard() { @@ -731,9 +732,9 @@ class Module } /** - * Return this module's user backends + * Get the module's user backends * - * @return array + * @return array */ public function getUserBackends() { @@ -742,9 +743,9 @@ class Module } /** - * Return this module's user group backends + * Get the module's user group backends * - * @return array + * @return array */ public function getUserGroupBackends() { @@ -755,10 +756,10 @@ class Module /** * Provide a named permission * - * @param string $name Unique permission name - * @param string $name Permission description + * @param string $name Unique permission name + * @param string $description Permission description * - * @return void + * @throws IcingaException If the permission is already provided */ protected function providePermission($name, $description) { @@ -777,10 +778,10 @@ class Module /** * Provide a named restriction * - * @param string $name Unique restriction name - * @param string $description Restriction description + * @param string $name Unique restriction name + * @param string $description Restriction description * - * @return void + * @throws IcingaException If the restriction is already provided */ protected function provideRestriction($name, $description) { @@ -799,15 +800,16 @@ class Module /** * Provide a module config tab * - * @param string $name Unique tab name - * @param string $config Tab config + * @param string $name Unique tab name + * @param array $config Tab config * - * @return $this + * @return $this + * @throws ProgrammingError If $config lacks the key 'url' */ protected function provideConfigTab($name, $config = array()) { if (! array_key_exists('url', $config)) { - throw new ProgrammingError('A module config tab MUST provide and "url"'); + throw new ProgrammingError('A module config tab MUST provide a "url"'); } $config['url'] = $this->getName() . '/' . ltrim($config['url'], '/'); $this->configTabs[$name] = $config; @@ -817,7 +819,7 @@ class Module /** * Provide a setup wizard * - * @param string $className The name of the class + * @param string $className The name of the class * * @return $this */ @@ -830,8 +832,8 @@ class Module /** * Provide a user backend capable of authenticating users * - * @param string $identifier The identifier of the new backend type - * @param string $className The name of the class + * @param string $identifier The identifier of the new backend type + * @param string $className The name of the class * * @return $this */ @@ -844,8 +846,8 @@ class Module /** * Provide a user group backend * - * @param string $identifier The identifier of the new backend type - * @param string $className The name of the class + * @param string $identifier The identifier of the new backend type + * @param string $className The name of the class * * @return $this */ @@ -856,7 +858,7 @@ class Module } /** - * Register new namespaces on the autoloader + * Register module namespaces on the autoloader * * @return $this */ @@ -897,7 +899,7 @@ class Module } /** - * return bool Whether this module has translations + * Get whether the module has translations */ public function hasLocales() { @@ -907,7 +909,7 @@ class Module /** * List all available locales * - * return array Locale list + * @return array Locale list */ public function listLocales() { @@ -954,10 +956,9 @@ class Module } /** - * Add routes for static content and any route added via addRoute() to the route chain + * Add routes for static content and any route added via {@link addRoute()} to the route chain * - * @return $this - * @see addRoute() + * @return $this */ protected function registerRoutes() { @@ -1006,14 +1007,14 @@ class Module /** * Include a php script if it is readable * - * @param string $file File to include + * @param string $file File to include * - * @return $this + * @return $this */ protected function includeScript($file) { - if (file_exists($file) && is_readable($file) === true) { - include($file); + if (file_exists($file) && is_readable($file)) { + include $file; } return $this; @@ -1021,29 +1022,27 @@ class Module /** * Run module config script + * + * @return $this */ protected function launchConfigScript() { if ($this->triedToLaunchConfigScript) { - return; + return $this; } $this->triedToLaunchConfigScript = true; - if (! file_exists($this->configScript) - || ! is_readable($this->configScript)) { - return; - } $this->registerAutoloader(); - include($this->configScript); + return $this->includeScript($this->configScript); } /** * Register hook * - * @param string $name - * @param string $class - * @param string $key + * @param string $name + * @param string $class + * @param string $key * - * @return $this + * @return $this */ protected function registerHook($name, $class, $key = null) { @@ -1072,12 +1071,8 @@ class Module } /** - * Translate a string with the global mt() - * - * @param $string - * @param null $context - * - * @return mixed|string + * (non-PHPDoc) + * @see Translator::translate() For the function documentation. */ protected function translate($string, $context = null) { From 8358e9165c49f7134feafeb5e3449ee9f464f04f Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 15:11:21 +0200 Subject: [PATCH 19/32] Don't require the module permission for the default module refs #9644 --- library/Icinga/Web/Controller/ModuleActionController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Web/Controller/ModuleActionController.php b/library/Icinga/Web/Controller/ModuleActionController.php index b6d2274eb..21a40900e 100644 --- a/library/Icinga/Web/Controller/ModuleActionController.php +++ b/library/Icinga/Web/Controller/ModuleActionController.php @@ -35,7 +35,9 @@ class ModuleActionController extends ActionController $this->_helper->layout()->moduleName = $this->moduleName; $this->view->translationDomain = $this->moduleName; $this->moduleInit(); - $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->moduleName); + if ($this->getFrontController()->getDefaultModule() !== $this->moduleName) { + $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->moduleName); + } } /** From a51f0ad65e71d8ec5c1115947b897dd06f3df877 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Fri, 24 Jul 2015 16:13:08 +0200 Subject: [PATCH 20/32] Roles: Include permissions and restrictions of unloaded modules too Because we now have module permissions it makes sense to allow an admin to configure permissions and restrictions from a module before allowing access to it. refs #9644 --- application/forms/Security/RoleForm.php | 41 ++++++++++++------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 19307262d..dba7ebe9f 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -71,28 +71,27 @@ class RoleForm extends ConfigForm $moduleName, $modulePermission ); - if ($mm->hasEnabled($moduleName)) { - $module = $mm->getModule($moduleName); - foreach ($module->getProvidedPermissions() as $permission) { - /** @var object $permission */ - $this->providedPermissions[$permission->name] = $permission->description - . ' (' . $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[$name] = array( - 'description' => $restriction->description, - 'name' => $restriction->name - ); + + $module = $mm->getModule($moduleName, false); + foreach ($module->getProvidedPermissions() as $permission) { + /** @var object $permission */ + $this->providedPermissions[$permission->name] = $permission->description + . ' (' . $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[$name] = array( + 'description' => $restriction->description, + 'name' => $restriction->name + ); } } } From ec5bebea95ce46842b6436355f7dd6e987940020 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 27 Jul 2015 11:30:44 +0200 Subject: [PATCH 21/32] doc/installation: Add upgrading note about module permissions refs #9644 --- doc/installation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/installation.md b/doc/installation.md index 708d45935..83939b143 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -290,3 +290,9 @@ The first release candidate of Icinga Web 2 introduces the following non-backwar the [EPEL repository](http://fedoraproject.org/wiki/EPEL). Before, Zend was installed as Icinga Web 2 vendor library through the package `icingaweb2-vendor-zend`. After upgrading, please make sure to remove the package `icingaweb2-vendor-zend`. + +* Icinga Web 2 version 2.0.0 requires permissions for accessing modules. Those permissions are automatically generated +for each installed module in the format `module/`. Administrators have to grant the module permissions to +users and/or user groups in the roles configuration for permitting access to specific modules. +In addition, restrictions provided by modules are now configurable for each installed module too. Before, +a module had to be enabled before having the possibility to configure restrictions. From de29d6600533b3e4a9c9f68459cbdc8644a0c33e Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 27 Jul 2015 15:07:43 +0200 Subject: [PATCH 22/32] doc/security: Add note about module permissions refs #9644 --- doc/security.md | 58 ++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/doc/security.md b/doc/security.md index 5e9293e44..3b8731a7d 100644 --- a/doc/security.md +++ b/doc/security.md @@ -3,9 +3,9 @@ Access control is a vital part of configuring Icinga Web 2 in a secure way. It is important that not every user that has access to Icinga Web 2 is able to do any action or to see any host and service. For example, it is useful to allow -only a small group of administrators to change the Icinga Web 2 configuration, -to prevent misconfiguration or security breaches. Another important use case is -creating groups of users which can only see the fraction of the monitoring +only a small group of administrators to change the Icinga Web 2 configuration, +to prevent misconfiguration or security breaches. Another important use case is +creating groups of users which can only see the fraction of the monitoring environment they are in charge of. This chapter will describe how to do the security configuration of Icinga Web 2 @@ -13,16 +13,16 @@ and how to apply permissions and restrictions to users or groups of users. ## Basics -Icinga Web 2 access control is done by defining **roles** that associate permissions -and restrictions with **users** and **groups**. There are two general kinds of +Icinga Web 2 access control is done by defining **roles** that associate permissions +and restrictions with **users** and **groups**. There are two general kinds of things to which access can be managed: actions and objects. ### Actions Actions are all the things an Icinga Web 2 user can do, like changing a certain configuration, -changing permissions or sending a command to the Icinga instance through the -Command Pipe +changing permissions or sending a command to the Icinga instance through the +Command Pipe in the monitoring module. All actions must be be **allowed explicitly** using permissions. A permission is a simple list of identifiers of actions a user is @@ -43,7 +43,7 @@ in greater detail in the section [Restrictions](#restrictions). Anyone who can **login** to Icinga Web 2 is considered a user and can be referenced to by the **user name** used during login. For example, there might be user called **jdoe** authenticated -using Active Directory, and a user **icingaadmin** that is authenticated using a MySQL-Database as backend. +using Active Directory, and a user **icingaadmin** that is authenticated using a MySQL-Database as backend. In the configuration, both can be referenced to by using their user names **icingaadmin** or **jdoe**. Icinga Web 2 users and groups are not configured by a configuration file, but provided by @@ -87,7 +87,7 @@ users have access to all configuration options, or another role **support** could define that a list of users or groups is restricted to see only hosts and services that match a specific query. -The actual permission of a certain user will be determined by merging the permissions +The actual permission of a certain user will be determined by merging the permissions and restrictions of the user itself and all the groups the user is member of. Permissions can be simply added up, while restrictions follow a slighty more complex pattern, that is described in the section [Stacking Filters](#stacking-filters). @@ -126,12 +126,12 @@ Each role is defined as a section, with the name of the role as section name. Th attributes can be defined for each role in a default Icinga Web 2 installation: - Directive | Description + Directive | Description ---------------------------|----------------------------------------------------------------------------- - users | A comma-separated list of user **user names** that are affected by this role - groups | A comma-separated list of **group names** that are affected by this role - permissions | A comma-separated list of **permissions** granted by this role - monitoring/filter/objects | A **filter expression** that restricts the access to services and hosts + users | A comma-separated list of user **user names** that are affected by this role + groups | A comma-separated list of **group names** that are affected by this role + permissions | A comma-separated list of **permissions** granted by this role + monitoring/filter/objects | A **filter expression** that restricts the access to services and hosts @@ -149,35 +149,39 @@ are in the namespace `config/modules` The permission `config/*` would grant permission to all configuration actions, while just specifying a wildcard `*` would give permission for all actions. +Access to modules is restricted to users who have the related module permission granted. Icinga Web 2 provides +a module permission in the format `module/` for each installed module. + When multiple roles assign permissions to the same user (either directly or indirectly -through a group) all permissions can simply be added together to get the users actual permission set. +through a group) all permissions are added together to get the users actual permission set. -#### Global permissions +### Global Permissions - Name | Permits --------------------------------------|----------------------------------------------------------------- - * | Allow everything, including module-specific permissions - config/* | Allow all configuration actions - config/modules | Allow enabling or disabling modules +Name | Permits +--------------- ----|-------------------------------------------------------- +* | Allow everything, including module-specific permissions +config/* | Allow all configuration actions +config/modules | Allow enabling or disabling modules +module/ | Allow access to module -#### Monitoring module permissions +### Monitoring Module Permissions The built-in monitoring module defines an additional set of permissions, that -is described in detail in [monitoring module documentation](/icingaweb2/doc/module/doc/chapter/monitoring-security#monitoring-security). +is described in detail in the [monitoring module documentation](/icingaweb2/doc/module/doc/chapter/monitoring-security#monitoring-security). ## Restrictions Restrictions can be used to define what a user or group can see by specifying -a filter expression that applies to a defined set of data. By default, when no -restrictions are defined, a user will be able to see every information that is available. +a filter expression that applies to a defined set of data. By default, when no +restrictions are defined, a user will be able to see every information that is available. A restrictions is always specified for a certain **filter directive**, that defines what data the filter is applied to. The **filter directive** is a simple identifier, that was defined in an Icinga Web 2 module. The only filter directive that is available in a default installation, is the `monitoring/filter/objects` directive, defined by the monitoring module, -that can be used to apply filter to hosts and services. This directive was previously +that can be used to apply filter to hosts and services. This directive was previously mentioned in the section [Syntax](#syntax). ### Filter Expressions @@ -251,7 +255,7 @@ Notice that because of the behavior of two stacking filters, a user that is memb #### Example 2: Hostgroups [unix-server] - groups = "unix-admins" + groups = "unix-admins" monitoring/filter/objects = "(hostgroup_name=bsd-servers|hostgroup_name=linux-servers)" This role allows all members of the group unix-admins to see hosts and services From 07fb82357bbd47ee865dcf1e535af0a37f16c0ef Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 27 Jul 2015 15:19:32 +0200 Subject: [PATCH 23/32] doc: Make documentation for all installed modules avaiable Before, only documentation of enabled modules was available. refs #9644 --- .../controllers/ModuleController.php | 32 ++++++++----------- .../views/scripts/index/index.phtml | 2 +- .../views/scripts/module/index.phtml | 2 +- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index eb9f4dcf1..3210de3f6 100644 --- a/modules/doc/application/controllers/ModuleController.php +++ b/modules/doc/application/controllers/ModuleController.php @@ -49,24 +49,24 @@ class Doc_ModuleController extends DocController { $moduleManager = Icinga::app()->getModuleManager(); $modules = array(); - foreach ($moduleManager->listEnabledModules() as $module) { + foreach ($moduleManager->listInstalledModules() as $module) { $path = $this->getPath($module, $moduleManager->getModuleDir($module, '/doc'), true); if ($path !== null) { - $modules[] = $moduleManager->getModule($module); + $modules[] = $moduleManager->getModule($module, false); } } $this->view->modules = $modules; } /** - * Assert that the given module is enabled + * Assert that the given module is installed * - * @param $moduleName + * @param string $moduleName * - * @throws Zend_Controller_Action_Exception If the required parameter 'moduleName' is empty or either if the - * given module is neither installed nor enabled + * @throws Zend_Controller_Action_Exception If the required parameter 'moduleName' is empty or if the + * given module is not installed */ - protected function assertModuleEnabled($moduleName) + protected function assertModuleInstalled($moduleName) { if (empty($moduleName)) { throw new Zend_Controller_Action_Exception( @@ -81,23 +81,17 @@ class Doc_ModuleController extends DocController 404 ); } - if (! $moduleManager->hasEnabled($moduleName)) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Module \'%s\' is not enabled'), $moduleName), - 404 - ); - } } /** * View the toc of a module's documentation * - * @see assertModuleEnabled() + * @see assertModuleInstalled() */ public function tocAction() { $module = $this->getParam('moduleName'); - $this->assertModuleEnabled($module); + $this->assertModuleInstalled($module); $this->view->moduleName = $module; try { $this->renderToc( @@ -116,12 +110,12 @@ class Doc_ModuleController extends DocController * * @throws Zend_Controller_Action_Exception If the required parameter 'chapterId' is missing or if an error in * the documentation module's library occurs - * @see assertModuleEnabled() + * @see assertModuleInstalled() */ public function chapterAction() { $module = $this->getParam('moduleName'); - $this->assertModuleEnabled($module); + $this->assertModuleInstalled($module); $chapter = $this->getParam('chapter'); if ($chapter === null) { throw new Zend_Controller_Action_Exception( @@ -145,12 +139,12 @@ class Doc_ModuleController extends DocController /** * View a module's documentation as PDF * - * @see assertModuleEnabled() + * @see assertModuleInstalled() */ public function pdfAction() { $module = $this->getParam('moduleName'); - $this->assertModuleEnabled($module); + $this->assertModuleInstalled($module); $this->renderPdf( $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')), $module, diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml index 67ee52ac1..af9ff087a 100644 --- a/modules/doc/application/views/scripts/index/index.phtml +++ b/modules/doc/application/views/scripts/index/index.phtml @@ -14,7 +14,7 @@ $this->translate('Module documentations'), 'doc/module/', null, - array('title' => $this->translate('List all modifications for which documentation is available')) + array('title' => $this->translate('List all modules for which documentation is available')) ); ?> diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml index a4d9276cc..d80e05cb3 100644 --- a/modules/doc/application/views/scripts/module/index.phtml +++ b/modules/doc/application/views/scripts/module/index.phtml @@ -4,7 +4,7 @@
    - +
  • qlink( $module->getTitle(), 'doc/module/toc', From 2c8235d4869e99e458732989b9f3f2c4e8a0547f Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Mon, 27 Jul 2015 16:25:41 +0200 Subject: [PATCH 24/32] lib: Introduce IcingaException::create() refs #9644 --- library/Icinga/Exception/IcingaException.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/library/Icinga/Exception/IcingaException.php b/library/Icinga/Exception/IcingaException.php index ab6591b01..62c7942be 100644 --- a/library/Icinga/Exception/IcingaException.php +++ b/library/Icinga/Exception/IcingaException.php @@ -4,6 +4,7 @@ namespace Icinga\Exception; use Exception; +use ReflectionClass; class IcingaException extends Exception { @@ -25,4 +26,17 @@ class IcingaException extends Exception } parent::__construct(vsprintf($message, $args), 0, $exc); } + + /** + * Create the exception from an array of arguments + * + * @param array $args + * + * @return static + */ + public static function create(array $args) + { + $e = new ReflectionClass(get_called_class()); + return $e->newInstanceArgs($args); + } } From 78285b95a3bf530594221d7f06115b01a5ba5da1 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 10:40:02 +0200 Subject: [PATCH 25/32] Make Controller::httpNotFound() variadic refs #9644 --- library/Icinga/Web/Controller.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Web/Controller.php b/library/Icinga/Web/Controller.php index a8d0545cf..2b114e9c2 100644 --- a/library/Icinga/Web/Controller.php +++ b/library/Icinga/Web/Controller.php @@ -53,13 +53,14 @@ class Controller extends ModuleActionController /** * Immediately respond w/ HTTP 404 * - * @param $message + * @param string $message Exception message or exception format string + * @param mixed ...$arg Format string argument * * @throws HttpNotFoundException */ public function httpNotFound($message) { - throw new HttpNotFoundException($message); + throw HttpNotFoundException::create(func_get_args()); } /** From a68c05da7f0832afb4329fc4326d10274fb09794 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 10:43:17 +0200 Subject: [PATCH 26/32] lib: Require a message when throwing IcingaExceptions refs #9644 --- library/Icinga/Exception/IcingaException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Exception/IcingaException.php b/library/Icinga/Exception/IcingaException.php index 62c7942be..a3ccec3f4 100644 --- a/library/Icinga/Exception/IcingaException.php +++ b/library/Icinga/Exception/IcingaException.php @@ -15,7 +15,7 @@ class IcingaException extends Exception * * If there is at least one exception, the last one will be also used for the exception chaining. */ - public function __construct($message = '') + public function __construct($message) { $args = array_slice(func_get_args(), 1); $exc = null; From b84650dd4b8b3ff63dbed8d38c64b26cbafa0a80 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 10:45:00 +0200 Subject: [PATCH 27/32] Document variadic function IcingaException::__construct properly refs #9644 --- library/Icinga/Exception/IcingaException.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/Icinga/Exception/IcingaException.php b/library/Icinga/Exception/IcingaException.php index a3ccec3f4..e0c954f31 100644 --- a/library/Icinga/Exception/IcingaException.php +++ b/library/Icinga/Exception/IcingaException.php @@ -9,11 +9,12 @@ use ReflectionClass; class IcingaException extends Exception { /** - * @param string $message format string for vsprintf() - * Any futher args: args for vsprintf() - * @see vsprintf + * Create a new exception * - * If there is at least one exception, the last one will be also used for the exception chaining. + * @param string $message Exception message or exception format string + * @param mixed ...$arg Format string argument + * + * If there is at least one exception, the last one will be used for exception chaining. */ public function __construct($message) { From fe73d6de834aa7cb407e8fc239afb09f903316d3 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 13:46:32 +0200 Subject: [PATCH 28/32] Remove superfluous whitespaces in IcingaException refs #9644 --- library/Icinga/Exception/IcingaException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Exception/IcingaException.php b/library/Icinga/Exception/IcingaException.php index 1869e75ca..3fdd5aa12 100644 --- a/library/Icinga/Exception/IcingaException.php +++ b/library/Icinga/Exception/IcingaException.php @@ -40,7 +40,7 @@ class IcingaException extends Exception $e = new ReflectionClass(get_called_class()); return $e->newInstanceArgs($args); } - + /** * Return the given exception formatted as one-liner * From 9846dfb2e2576181f7f18c74fe85eebeeec6573d Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 13:47:06 +0200 Subject: [PATCH 29/32] doc: Use Controller:httpNotFound() in ModuleController refs #9644 --- .../application/controllers/ModuleController.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index 9b8d1d1cf..0608e0ad6 100644 --- a/modules/doc/application/controllers/ModuleController.php +++ b/modules/doc/application/controllers/ModuleController.php @@ -35,10 +35,7 @@ class Doc_ModuleController extends DocController if ($suppressErrors) { return null; } - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Documentation for module \'%s\' is not available'), $module), - 404 - ); + $this->httpNotFound($this->translate('Documentation for module \'%s\' is not available'), $module); } /** @@ -75,10 +72,7 @@ class Doc_ModuleController extends DocController } $moduleManager = Icinga::app()->getModuleManager(); if (! $moduleManager->hasInstalled($moduleName)) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Module \'%s\' is not installed'), $moduleName), - 404 - ); + $this->httpNotFound($this->translate('Module \'%s\' is not installed'), $moduleName); } } @@ -100,7 +94,7 @@ class Doc_ModuleController extends DocController array('moduleName' => $module) ); } catch (DocException $e) { - throw new Zend_Controller_Action_Exception($e->getMessage(), 404); + $this->httpNotFound($e->getMessage()); } } @@ -131,7 +125,7 @@ class Doc_ModuleController extends DocController array('moduleName' => $module) ); } catch (DocException $e) { - throw new Zend_Controller_Action_Exception($e->getMessage(), 404); + $this->httpNotFound($e->getMessage()); } } From 9a758f3ed9d961e41de8ca1eaf098b084299a1bd Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 13:47:25 +0200 Subject: [PATCH 30/32] doc: Use Controller::httpNotFound() in SearchController refs #9644 --- modules/doc/application/controllers/SearchController.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modules/doc/application/controllers/SearchController.php b/modules/doc/application/controllers/SearchController.php index e896c31f6..5d71681eb 100644 --- a/modules/doc/application/controllers/SearchController.php +++ b/modules/doc/application/controllers/SearchController.php @@ -92,9 +92,6 @@ class Doc_SearchController extends DocController return $path; } } - throw new Zend_Controller_Action_Exception( - $this->translate('Documentation for Icinga Web 2 is not available'), - 404 - ); + $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available')); } } From 794e4a1e1edf8ce51d1ef35704596ffc6ec4f3b7 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 13:59:39 +0200 Subject: [PATCH 31/32] doc: Use Params::getRequired() in IcingawebController refs #9644 --- .../application/controllers/IcingawebController.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php index 8af8b97ff..9fccd32fa 100644 --- a/modules/doc/application/controllers/IcingawebController.php +++ b/modules/doc/application/controllers/IcingawebController.php @@ -38,17 +38,11 @@ class Doc_IcingawebController extends DocController /** * View a chapter of Icinga Web 2's documentation * - * @throws Zend_Controller_Action_Exception If the required parameter 'chapterId' is missing + * @throws \Icinga\Exception\MissingParameterException If the required parameter 'chapter' is missing */ public function chapterAction() { - $chapter = $this->getParam('chapter'); - if ($chapter === null) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Missing parameter %s'), 'chapter'), - 404 - ); - } + $chapter = $this->params->getRequired('chapter'); $this->renderChapter( $this->getPath(), $chapter, From 5c5dea616db7a5c6cecd360efdf0e583e4482526 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Tue, 28 Jul 2015 13:59:59 +0200 Subject: [PATCH 32/32] doc: Use Params::getRequired() in ModuleController refs #9644 --- .../controllers/ModuleController.php | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index 0608e0ad6..6faf7cc12 100644 --- a/modules/doc/application/controllers/ModuleController.php +++ b/modules/doc/application/controllers/ModuleController.php @@ -10,16 +10,15 @@ class Doc_ModuleController extends DocController /** * Get the path to a module documentation * - * @param string $module The name of the module - * @param string $default The default path - * @param bool $suppressErrors Whether to not throw an exception if the module documentation is not - * available + * @param string $module The name of the module + * @param string $default The default path + * @param bool $suppressErrors Whether to not throw an exception if the module documentation is not available * - * @return string|null Path to the documentation or null if the module documentation is not - * available and errors are suppressed + * @return string|null Path to the documentation or null if the module documentation is not available + * and errors are suppressed * - * @throws Zend_Controller_Action_Exception If the module documentation is not available and errors are not - * suppressed + * @throws \Icinga\Exception\Http\HttpNotFoundException If the module documentation is not available and errors + * are not suppressed */ protected function getPath($module, $default, $suppressErrors = false) { @@ -59,17 +58,10 @@ class Doc_ModuleController extends DocController * * @param string $moduleName * - * @throws Zend_Controller_Action_Exception If the required parameter 'moduleName' is empty or if the - * given module is not installed + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed */ protected function assertModuleInstalled($moduleName) { - if (empty($moduleName)) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Missing parameter \'%s\''), 'moduleName'), - 404 - ); - } $moduleManager = Icinga::app()->getModuleManager(); if (! $moduleManager->hasInstalled($moduleName)) { $this->httpNotFound($this->translate('Module \'%s\' is not installed'), $moduleName); @@ -79,11 +71,13 @@ class Doc_ModuleController extends DocController /** * View the toc of a module's documentation * - * @see assertModuleInstalled() + * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed + * @see assertModuleInstalled() */ public function tocAction() { - $module = $this->getParam('moduleName'); + $module = $this->params->getRequired('moduleName'); $this->assertModuleInstalled($module); $this->view->moduleName = $module; try { @@ -101,21 +95,16 @@ class Doc_ModuleController extends DocController /** * View a chapter of a module's documentation * - * @throws Zend_Controller_Action_Exception If the required parameter 'chapterId' is missing or if an error in - * the documentation module's library occurs + * @throws \Icinga\Exception\MissingParameterException If one of the required parameters 'moduleName' and + * 'chapter' is empty + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed * @see assertModuleInstalled() */ public function chapterAction() { - $module = $this->getParam('moduleName'); + $module = $this->params->getRequired('moduleName'); $this->assertModuleInstalled($module); - $chapter = $this->getParam('chapter'); - if ($chapter === null) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Missing parameter %s'), 'chapter'), - 404 - ); - } + $chapter = $this->params->getRequired('chapter'); $this->view->moduleName = $module; try { $this->renderChapter( @@ -132,11 +121,13 @@ class Doc_ModuleController extends DocController /** * View a module's documentation as PDF * - * @see assertModuleInstalled() + * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty + * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed + * @see assertModuleInstalled() */ public function pdfAction() { - $module = $this->getParam('moduleName'); + $module = $this->params->getRequired('moduleName'); $this->assertModuleInstalled($module); $this->renderPdf( $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),