diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php index 543a84217..cb2c31855 100644 --- a/application/controllers/ConfigController.php +++ b/application/controllers/ConfigController.php @@ -95,6 +95,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') @@ -120,6 +121,7 @@ class ConfigController extends Controller public function moduleAction() { + $this->assertPermission('config/modules'); $app = Icinga::app(); $manager = $app->getModuleManager(); $name = $this->getParam('name'); 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(); diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php index 114ddfc1f..dba7ebe9f 100644 --- a/application/forms/Security/RoleForm.php +++ b/application/forms/Security/RoleForm.php @@ -63,24 +63,34 @@ class RoleForm extends ConfigForm public function init() { $helper = new Zend_Form_Element('bogus'); - foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { + $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 + ); + + $module = $mm->getModule($moduleName, false); 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 - // \x7f to \xff (127 to 255) + // 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 + 'description' => $restriction->description, + 'name' => $restriction->name ); } } 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 + 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. 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 diff --git a/library/Icinga/Application/Modules/Manager.php b/library/Icinga/Application/Modules/Manager.php index e3e5ec6f2..8a3970841 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 * @@ -401,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 + ); } /** diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 54e23d02c..ad1ec1f5d 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; @@ -113,6 +113,13 @@ class Module */ private $triedToLaunchConfigScript = false; + /** + * Whether the module's namespaces have been registered on our autoloader + * + * @var bool + */ + protected $registeredAutoloader = false; + /** * Whether this module has been registered * @@ -199,87 +206,6 @@ class Module */ protected $userGroupBackends = array(); - /** - * Provide a search URL - * - * @param string $title - * @param string $url - * @param int $priority - */ - public function provideSearchUrl($title, $url, $priority = 0) - { - $searchUrl = (object) array( - 'title' => (string) $title, - 'url' => (string) $url, - 'priority' => (int) $priority - ); - - $this->searchUrls[] = $searchUrl; - } - - /** - * Return this module's search urls - * - * @return array - */ - public function getSearchUrls() - { - $this->launchConfigScript(); - return $this->searchUrls; - } - - /** - * Get all Menu Items - * - * @return array - */ - public function getPaneItems() - { - $this->launchConfigScript(); - return $this->paneItems; - } - - /** - * Add a pane to dashboard - * - * @param $name - * @return Pane - */ - protected function dashboard($name) - { - $this->paneItems[$name] = new Pane($name); - return $this->paneItems[$name]; - } - - /** - * Get all Menu Items - * - * @return array - */ - public function getMenuItems() - { - $this->launchConfigScript(); - return $this->menuItems; - } - - /** - * Add a menu Section to the Sidebar menu - * - * @param $name - * @param array $properties - * @return mixed - */ - protected function menuSection($name, array $properties = array()) - { - if (array_key_exists($name, $this->menuItems)) { - $this->menuItems[$name]->setProperties($properties); - } else { - $this->menuItems[$name] = new Menu($name, new ConfigObject($properties)); - } - - return $this->menuItems[$name]; - } - /** * Create a new module object * @@ -304,6 +230,91 @@ class Module $this->metadataFile = $basedir . '/module.info'; } + /** + * Provide a search URL + * + * @param string $title + * @param string $url + * @param int $priority + * + * @return $this + */ + public function provideSearchUrl($title, $url, $priority = 0) + { + $this->searchUrls[] = (object) array( + 'title' => (string) $title, + 'url' => (string) $url, + 'priority' => (int) $priority + ); + + return $this; + } + + /** + * Get this module's search urls + * + * @return array + */ + public function getSearchUrls() + { + $this->launchConfigScript(); + return $this->searchUrls; + } + + /** + * Get all pane items + * + * @return array + */ + public function getPaneItems() + { + $this->launchConfigScript(); + return $this->paneItems; + } + + /** + * Add a pane to dashboard + * + * @param string $name + * + * @return Pane + */ + protected function dashboard($name) + { + $this->paneItems[$name] = new Pane($name); + return $this->paneItems[$name]; + } + + /** + * Get all menu items + * + * @return array + */ + public function getMenuItems() + { + $this->launchConfigScript(); + return $this->menuItems; + } + + /** + * Add or get a menu section + * + * @param string $name + * @param array $properties + * + * @return Menu + */ + protected function menuSection($name, array $properties = array()) + { + if (array_key_exists($name, $this->menuItems)) { + $this->menuItems[$name]->setProperties($properties); + } else { + $this->menuItems[$name] = new Menu($name, new ConfigObject($properties)); + } + + return $this->menuItems[$name]; + } + /** * Register module * @@ -327,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() { @@ -346,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) { @@ -356,7 +367,7 @@ class Module } /** - * Get module by name + * Get a module by name * * @param string $name * @param bool $autoload @@ -418,7 +429,7 @@ class Module } /** - * Getter for module name + * Get the module name * * @return string */ @@ -428,7 +439,7 @@ class Module } /** - * Getter for module version + * Get the module version * * @return string */ @@ -438,7 +449,7 @@ class Module } /** - * Get module description + * Get the module description * * @return string */ @@ -448,7 +459,7 @@ class Module } /** - * Get module title (short description) + * Get the module title (short description) * * @return string */ @@ -458,9 +469,9 @@ class Module } /** - * Getter for module version + * Get the module dependencies * - * @return Array + * @return array */ public function getDependencies() { @@ -555,7 +566,7 @@ class Module } /** - * Getter for css file name + * Get the module's CSS directory * * @return string */ @@ -565,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 */ @@ -585,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 */ @@ -595,7 +606,7 @@ class Module } /** - * Getter for configuration directory + * Get the module's configuration directory * * @return string */ @@ -605,7 +616,7 @@ class Module } /** - * Getter for form directory + * Get the module's form directory * * @return string */ @@ -615,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') { @@ -627,9 +638,7 @@ class Module } /** - * Retrieve provided permissions - * - * @param string $name Permission name + * Get provided permissions * * @return array */ @@ -640,9 +649,8 @@ class Module } /** - * Retrieve provided restrictions + * Get provided restrictions * - * @param string $name Restriction name * @return array */ public function getProvidedRestrictions() @@ -652,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) { @@ -678,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() { @@ -698,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() { @@ -714,9 +722,9 @@ class Module } /** - * Return this module's setup wizard + * Get the module's setup wizard * - * @return SetupWizard + * @return SetupWizard */ public function getSetupWizard() { @@ -724,9 +732,9 @@ class Module } /** - * Return this module's user backends + * Get the module's user backends * - * @return array + * @return array */ public function getUserBackends() { @@ -735,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() { @@ -748,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) { @@ -770,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) { @@ -792,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; @@ -810,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 */ @@ -823,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 */ @@ -837,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 */ @@ -849,12 +858,16 @@ class Module } /** - * Register new namespaces on the autoloader + * Register module namespaces on the autoloader * * @return $this */ protected function registerAutoloader() { + if ($this->registeredAutoloader) { + return $this; + } + $moduleName = ucfirst($this->getName()); $moduleLibraryDir = $this->getLibDir(). '/'. $moduleName; @@ -867,6 +880,8 @@ class Module $this->app->getLoader()->registerNamespace('Icinga\\Module\\' . $moduleName. '\\Forms', $moduleFormDir); } + $this->registeredAutoloader = true; + return $this; } @@ -884,7 +899,7 @@ class Module } /** - * return bool Whether this module has translations + * Get whether the module has translations */ public function hasLocales() { @@ -894,7 +909,7 @@ class Module /** * List all available locales * - * return array Locale list + * @return array Locale list */ public function listLocales() { @@ -941,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() { @@ -993,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; @@ -1008,28 +1022,27 @@ class Module /** * Run module config script + * + * @return $this */ protected function launchConfigScript() { - if ($this->triedToLaunchConfigScript || !$this->registered) { - return; + if ($this->triedToLaunchConfigScript) { + return $this; } $this->triedToLaunchConfigScript = true; - if (! file_exists($this->configScript) - || ! is_readable($this->configScript)) { - return; - } - include($this->configScript); + $this->registerAutoloader(); + 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) { @@ -1058,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) { diff --git a/library/Icinga/Exception/IcingaException.php b/library/Icinga/Exception/IcingaException.php index bdc6833a3..3fdd5aa12 100644 --- a/library/Icinga/Exception/IcingaException.php +++ b/library/Icinga/Exception/IcingaException.php @@ -4,17 +4,19 @@ namespace Icinga\Exception; use Exception; +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 = '') + public function __construct($message) { $args = array_slice(func_get_args(), 1); $exc = null; @@ -26,6 +28,19 @@ 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); + } + /** * Return the given exception formatted as one-liner * diff --git a/library/Icinga/Web/Controller.php b/library/Icinga/Web/Controller.php index 86ef520c9..51eb0c2ee 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()); } /** diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php index 0fb71f816..2c4373a5c 100644 --- a/library/Icinga/Web/Controller/ActionController.php +++ b/library/Icinga/Web/Controller/ActionController.php @@ -155,7 +155,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); } } diff --git a/library/Icinga/Web/Controller/ModuleActionController.php b/library/Icinga/Web/Controller/ModuleActionController.php index 9286ff679..21a40900e 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 @@ -34,6 +35,9 @@ class ModuleActionController extends ActionController $this->_helper->layout()->moduleName = $this->moduleName; $this->view->translationDomain = $this->moduleName; $this->moduleInit(); + if ($this->getFrontController()->getDefaultModule() !== $this->moduleName) { + $this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->moduleName); + } } /** 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(); } 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(); } } 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 * 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, diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index 753da45cc..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) { @@ -35,10 +34,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); } /** @@ -48,55 +44,41 @@ 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 \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed */ - protected function assertModuleEnabled($moduleName) + 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)) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Module \'%s\' is not installed'), $moduleName), - 404 - ); - } - if (! $moduleManager->hasEnabled($moduleName)) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Module \'%s\' is not enabled'), $moduleName), - 404 - ); + $this->httpNotFound($this->translate('Module \'%s\' is not installed'), $moduleName); } } /** * View the toc of a module's documentation * - * @see assertModuleEnabled() + * @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'); - $this->assertModuleEnabled($module); + $module = $this->params->getRequired('moduleName'); + $this->assertModuleInstalled($module); $this->view->moduleName = $module; try { $this->renderToc( @@ -106,28 +88,23 @@ class Doc_ModuleController extends DocController array('moduleName' => $module) ); } catch (DocException $e) { - throw new Zend_Controller_Action_Exception($e->getMessage(), 404); + $this->httpNotFound($e->getMessage()); } } /** * 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 - * @see assertModuleEnabled() + * @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'); - $this->assertModuleEnabled($module); - $chapter = $this->getParam('chapter'); - if ($chapter === null) { - throw new Zend_Controller_Action_Exception( - sprintf($this->translate('Missing parameter %s'), 'chapter'), - 404 - ); - } + $module = $this->params->getRequired('moduleName'); + $this->assertModuleInstalled($module); + $chapter = $this->params->getRequired('chapter'); $this->view->moduleName = $module; try { $this->renderChapter( @@ -137,19 +114,21 @@ class Doc_ModuleController extends DocController array('moduleName' => $module) ); } catch (DocException $e) { - throw new Zend_Controller_Action_Exception($e->getMessage(), 404); + $this->httpNotFound($e->getMessage()); } } /** * View a module's documentation as PDF * - * @see assertModuleEnabled() + * @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'); - $this->assertModuleEnabled($module); + $module = $this->params->getRequired('moduleName'); + $this->assertModuleInstalled($module); $this->renderPdf( $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')), $module, 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')); } } 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', 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( 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');