Merge branch 'feature/secure-modules-9644'

resolves #9644
This commit is contained in:
Eric Lippmann 2015-07-28 14:00:27 +02:00
commit d2467fee16
22 changed files with 417 additions and 365 deletions

View File

@ -95,6 +95,7 @@ class ConfigController extends Controller
*/ */
public function modulesAction() public function modulesAction()
{ {
$this->assertPermission('config/modules');
// Overwrite tabs created in init // Overwrite tabs created in init
// @TODO(el): This seems not natural to me. Module configuration should have its own controller. // @TODO(el): This seems not natural to me. Module configuration should have its own controller.
$this->view->tabs = Widget::create('tabs') $this->view->tabs = Widget::create('tabs')
@ -120,6 +121,7 @@ class ConfigController extends Controller
public function moduleAction() public function moduleAction()
{ {
$this->assertPermission('config/modules');
$app = Icinga::app(); $app = Icinga::app();
$manager = $app->getModuleManager(); $manager = $app->getModuleManager();
$name = $this->getParam('name'); $name = $this->getParam('name');

View File

@ -12,7 +12,9 @@ class SearchController extends ActionController
{ {
public function indexAction() 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. // NOTE: This renders the dashboard twice. Remove this once we can catch exceptions thrown in view scripts.
$this->view->dashboard->render(); $this->view->dashboard->render();

View File

@ -63,16 +63,26 @@ class RoleForm extends ConfigForm
public function init() public function init()
{ {
$helper = new Zend_Form_Element('bogus'); $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) { foreach ($module->getProvidedPermissions() as $permission) {
/** @var object $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) { foreach ($module->getProvidedRestrictions() as $restriction) {
/** @var object $restriction */ /** @var object $restriction */
$name = $helper->filterName($restriction->name); // Zend only permits alphanumerics, the underscore, // Zend only permits alphanumerics, the underscore, the circumflex and any ASCII character in range
// the circumflex and any ASCII character in range
// \x7f to \xff (127 to 255) // \x7f to \xff (127 to 255)
$name = $helper->filterName($restriction->name);
while (isset($this->providedRestrictions[$name])) { while (isset($this->providedRestrictions[$name])) {
// Because Zend_Form_Element::filterName() replaces any not permitted character with the empty // 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' // string we may have duplicate names, e.g. 're/striction' and 'restriction'

View File

@ -1,8 +1,11 @@
<?php <?php
use Icinga\Web\Widget\SearchDashboard;
?>
<? if (SearchDashboard::search('dummy')->getPane('search')->hasDashlets()): ?> use Icinga\Web\Widget\SearchDashboard;
$searchDashboard = new SearchDashboard();
$searchDashboard->setUser($this->Auth()->getUser());
if ($searchDashboard->search('dummy')->getPane('search')->hasDashlets()): ?>
<form action="<?= $this->href('search') ?>" method="get" role="search"> <form action="<?= $this->href('search') ?>" method="get" role="search">
<input <input
type="text" name="q" id="search" class="search" placeholder="<?= $this->translate('Search') ?> &hellip;" type="text" name="q" id="search" class="search" placeholder="<?= $this->translate('Search') ?> &hellip;"

View File

@ -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 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 through the package `icingaweb2-vendor-zend`. After upgrading, please make sure to remove the package
`icingaweb2-vendor-zend`. `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/<moduleName>`. 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.

View File

@ -149,22 +149,26 @@ are in the namespace `config/modules`
The permission `config/*` would grant permission to all configuration actions, The permission `config/*` would grant permission to all configuration actions,
while just specifying a wildcard `*` would give permission for all actions. while just specifying a wildcard `*` would give permission for all actions.
When multiple roles assign permissions to the same user (either directly or indirectly Access to modules is restricted to users who have the related module permission granted. Icinga Web 2 provides
through a group) all permissions can simply be added together to get the users actual permission set. a module permission in the format `module/<moduleName>` for each installed module.
#### Global permissions When multiple roles assign permissions to the same user (either directly or indirectly
through a group) all permissions are added together to get the users actual permission set.
### Global Permissions
Name | Permits Name | Permits
-------------------------------------|----------------------------------------------------------------- --------------- ----|--------------------------------------------------------
* | Allow everything, including module-specific permissions * | Allow everything, including module-specific permissions
config/* | Allow all configuration actions config/* | Allow all configuration actions
config/modules | Allow enabling or disabling modules config/modules | Allow enabling or disabling modules
module/<moduleName> | Allow access to module <moduleName>
#### Monitoring module permissions ### Monitoring Module Permissions
The built-in monitoring module defines an additional set of permissions, that 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).
## <a id="restrictions"></a> Restrictions ## <a id="restrictions"></a> Restrictions

View File

@ -24,6 +24,13 @@ use Icinga\Exception\NotReadableError;
*/ */
class Manager class Manager
{ {
/**
* Namespace for module permissions
*
* @var string
*/
const MODULE_PERMISSION_NS = 'module/';
/** /**
* Array of all installed module's base directories * Array of all installed module's base directories
* *
@ -401,23 +408,26 @@ 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 * @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)) { if ($this->hasLoaded($name)) {
return $this->loadedModules[$name];
} elseif (! (bool) $assertLoaded) {
return new Module($this->app, $name, $this->getModuleDir($name));
}
throw new ProgrammingError( throw new ProgrammingError(
'Cannot access module %s as it hasn\'t been loaded', 'Can\'t access module %s because it hasn\'t been loaded',
$name $name
); );
} }
return $this->loadedModules[$name];
}
/** /**
* Return an array containing information objects for each available module * Return an array containing information objects for each available module

View File

@ -102,7 +102,7 @@ class Module
/** /**
* Module metadata (version...) * Module metadata (version...)
* *
* @var stdClass * @var object
*/ */
private $metadata; private $metadata;
@ -113,6 +113,13 @@ class Module
*/ */
private $triedToLaunchConfigScript = false; 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 * Whether this module has been registered
* *
@ -199,87 +206,6 @@ class Module
*/ */
protected $userGroupBackends = array(); 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 * Create a new module object
* *
@ -304,6 +230,91 @@ class Module
$this->metadataFile = $basedir . '/module.info'; $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 * Register module
* *
@ -327,14 +338,14 @@ class Module
); );
return false; return false;
} }
$this->registerWebIntegration(); $this->registerWebIntegration();
$this->registered = true; $this->registered = true;
return true; return true;
} }
/** /**
* Return whether this module has been registered * Get whether this module has been registered
* *
* @return bool * @return bool
*/ */
@ -348,7 +359,7 @@ class Module
* *
* @param string $name * @param string $name
* *
* @return boolean * @return bool
*/ */
public static function exists($name) public static function exists($name)
{ {
@ -356,7 +367,7 @@ class Module
} }
/** /**
* Get module by name * Get a module by name
* *
* @param string $name * @param string $name
* @param bool $autoload * @param bool $autoload
@ -418,7 +429,7 @@ class Module
} }
/** /**
* Getter for module name * Get the module name
* *
* @return string * @return string
*/ */
@ -428,7 +439,7 @@ class Module
} }
/** /**
* Getter for module version * Get the module version
* *
* @return string * @return string
*/ */
@ -438,7 +449,7 @@ class Module
} }
/** /**
* Get module description * Get the module description
* *
* @return string * @return string
*/ */
@ -448,7 +459,7 @@ class Module
} }
/** /**
* Get module title (short description) * Get the module title (short description)
* *
* @return string * @return string
*/ */
@ -458,9 +469,9 @@ class Module
} }
/** /**
* Getter for module version * Get the module dependencies
* *
* @return Array * @return array
*/ */
public function getDependencies() public function getDependencies()
{ {
@ -555,7 +566,7 @@ class Module
} }
/** /**
* Getter for css file name * Get the module's CSS directory
* *
* @return string * @return string
*/ */
@ -565,17 +576,7 @@ class Module
} }
/** /**
* Getter for base directory * Get the module's controller directory
*
* @return string
*/
public function getBaseDir()
{
return $this->basedir;
}
/**
* Get the controller directory
* *
* @return string * @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 * @return string
*/ */
@ -595,7 +606,7 @@ class Module
} }
/** /**
* Getter for configuration directory * Get the module's configuration directory
* *
* @return string * @return string
*/ */
@ -605,7 +616,7 @@ class Module
} }
/** /**
* Getter for form directory * Get the module's form directory
* *
* @return string * @return string
*/ */
@ -615,7 +626,7 @@ class Module
} }
/** /**
* Getter for module config object * Get the module config
* *
* @param string $file * @param string $file
* *
@ -627,9 +638,7 @@ class Module
} }
/** /**
* Retrieve provided permissions * Get provided permissions
*
* @param string $name Permission name
* *
* @return array * @return array
*/ */
@ -640,9 +649,8 @@ class Module
} }
/** /**
* Retrieve provided restrictions * Get provided restrictions
* *
* @param string $name Restriction name
* @return array * @return array
*/ */
public function getProvidedRestrictions() public function getProvidedRestrictions()
@ -652,20 +660,7 @@ class Module
} }
/** /**
* Whether the given permission name is supported * Whether the module provides the given restriction
*
* @param string $name Permission 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 * @param string $name Restriction 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() public function getConfigTabs()
{ {
@ -698,7 +706,7 @@ class Module
} }
/** /**
* Whether this module provides a setup wizard * Whether the module provides a setup wizard
* *
* @return bool * @return bool
*/ */
@ -714,7 +722,7 @@ class Module
} }
/** /**
* Return this module's setup wizard * Get the module's setup wizard
* *
* @return SetupWizard * @return SetupWizard
*/ */
@ -724,7 +732,7 @@ class Module
} }
/** /**
* Return this module's user backends * Get the module's user backends
* *
* @return array * @return array
*/ */
@ -735,7 +743,7 @@ class Module
} }
/** /**
* Return this module's user group backends * Get the module's user group backends
* *
* @return array * @return array
*/ */
@ -749,9 +757,9 @@ class Module
* Provide a named permission * Provide a named permission
* *
* @param string $name Unique permission name * @param string $name Unique permission name
* @param string $name Permission description * @param string $description Permission description
* *
* @return void * @throws IcingaException If the permission is already provided
*/ */
protected function providePermission($name, $description) protected function providePermission($name, $description)
{ {
@ -773,7 +781,7 @@ class Module
* @param string $name Unique restriction name * @param string $name Unique restriction name
* @param string $description Restriction description * @param string $description Restriction description
* *
* @return void * @throws IcingaException If the restriction is already provided
*/ */
protected function provideRestriction($name, $description) protected function provideRestriction($name, $description)
{ {
@ -793,14 +801,15 @@ class Module
* Provide a module config tab * Provide a module config tab
* *
* @param string $name Unique tab name * @param string $name Unique tab name
* @param string $config Tab config * @param array $config Tab config
* *
* @return $this * @return $this
* @throws ProgrammingError If $config lacks the key 'url'
*/ */
protected function provideConfigTab($name, $config = array()) protected function provideConfigTab($name, $config = array())
{ {
if (! array_key_exists('url', $config)) { 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'], '/'); $config['url'] = $this->getName() . '/' . ltrim($config['url'], '/');
$this->configTabs[$name] = $config; $this->configTabs[$name] = $config;
@ -849,12 +858,16 @@ class Module
} }
/** /**
* Register new namespaces on the autoloader * Register module namespaces on the autoloader
* *
* @return $this * @return $this
*/ */
protected function registerAutoloader() protected function registerAutoloader()
{ {
if ($this->registeredAutoloader) {
return $this;
}
$moduleName = ucfirst($this->getName()); $moduleName = ucfirst($this->getName());
$moduleLibraryDir = $this->getLibDir(). '/'. $moduleName; $moduleLibraryDir = $this->getLibDir(). '/'. $moduleName;
@ -867,6 +880,8 @@ class Module
$this->app->getLoader()->registerNamespace('Icinga\\Module\\' . $moduleName. '\\Forms', $moduleFormDir); $this->app->getLoader()->registerNamespace('Icinga\\Module\\' . $moduleName. '\\Forms', $moduleFormDir);
} }
$this->registeredAutoloader = true;
return $this; return $this;
} }
@ -884,7 +899,7 @@ class Module
} }
/** /**
* return bool Whether this module has translations * Get whether the module has translations
*/ */
public function hasLocales() public function hasLocales()
{ {
@ -894,7 +909,7 @@ class Module
/** /**
* List all available locales * List all available locales
* *
* return array Locale list * @return array Locale list
*/ */
public function listLocales() 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 * @return $this
* @see addRoute()
*/ */
protected function registerRoutes() protected function registerRoutes()
{ {
@ -999,8 +1013,8 @@ class Module
*/ */
protected function includeScript($file) protected function includeScript($file)
{ {
if (file_exists($file) && is_readable($file) === true) { if (file_exists($file) && is_readable($file)) {
include($file); include $file;
} }
return $this; return $this;
@ -1008,18 +1022,17 @@ class Module
/** /**
* Run module config script * Run module config script
*
* @return $this
*/ */
protected function launchConfigScript() protected function launchConfigScript()
{ {
if ($this->triedToLaunchConfigScript || !$this->registered) { if ($this->triedToLaunchConfigScript) {
return; return $this;
} }
$this->triedToLaunchConfigScript = true; $this->triedToLaunchConfigScript = true;
if (! file_exists($this->configScript) $this->registerAutoloader();
|| ! is_readable($this->configScript)) { return $this->includeScript($this->configScript);
return;
}
include($this->configScript);
} }
/** /**
@ -1058,12 +1071,8 @@ class Module
} }
/** /**
* Translate a string with the global mt() * (non-PHPDoc)
* * @see Translator::translate() For the function documentation.
* @param $string
* @param null $context
*
* @return mixed|string
*/ */
protected function translate($string, $context = null) protected function translate($string, $context = null)
{ {

View File

@ -4,17 +4,19 @@
namespace Icinga\Exception; namespace Icinga\Exception;
use Exception; use Exception;
use ReflectionClass;
class IcingaException extends Exception class IcingaException extends Exception
{ {
/** /**
* @param string $message format string for vsprintf() * Create a new exception
* Any futher args: args for vsprintf()
* @see vsprintf
* *
* 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); $args = array_slice(func_get_args(), 1);
$exc = null; $exc = null;
@ -26,6 +28,19 @@ class IcingaException extends Exception
parent::__construct(vsprintf($message, $args), 0, $exc); 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 * Return the given exception formatted as one-liner
* *

View File

@ -53,13 +53,14 @@ class Controller extends ModuleActionController
/** /**
* Immediately respond w/ HTTP 404 * Immediately respond w/ HTTP 404
* *
* @param $message * @param string $message Exception message or exception format string
* @param mixed ...$arg Format string argument
* *
* @throws HttpNotFoundException * @throws HttpNotFoundException
*/ */
public function httpNotFound($message) public function httpNotFound($message)
{ {
throw new HttpNotFoundException($message); throw HttpNotFoundException::create(func_get_args());
} }
/** /**

View File

@ -155,7 +155,7 @@ class ActionController extends Zend_Controller_Action
*/ */
public function assertPermission($permission) public function assertPermission($permission)
{ {
if (! $this->Auth()->hasPermission($permission)) { if ($this->requiresAuthentication && ! $this->Auth()->hasPermission($permission)) {
throw new SecurityException('No permission for %s', $permission); throw new SecurityException('No permission for %s', $permission);
} }
} }

View File

@ -5,6 +5,7 @@ namespace Icinga\Web\Controller;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Application\Modules\Manager;
/** /**
* Base class for module action controllers * Base class for module action controllers
@ -34,6 +35,9 @@ class ModuleActionController extends ActionController
$this->_helper->layout()->moduleName = $this->moduleName; $this->_helper->layout()->moduleName = $this->moduleName;
$this->view->translationDomain = $this->moduleName; $this->view->translationDomain = $this->moduleName;
$this->moduleInit(); $this->moduleInit();
if ($this->getFrontController()->getDefaultModule() !== $this->moduleName) {
$this->assertPermission(Manager::MODULE_PERMISSION_NS . $this->moduleName);
}
} }
/** /**

View File

@ -206,14 +206,15 @@ class Menu implements RecursiveIterator
*/ */
public static function load() public static function load()
{ {
/** @var $menu \Icinga\Web\Menu */
$menu = new static('menu'); $menu = new static('menu');
$menu->addMainMenuItems(); $menu->addMainMenuItems();
$auth = Manager::getInstance();
$manager = Icinga::app()->getModuleManager(); $manager = Icinga::app()->getModuleManager();
foreach ($manager->getLoadedModules() as $module) { foreach ($manager->getLoadedModules() as $module) {
/** @var $module \Icinga\Application\Modules\Module */ if ($auth->hasPermission($manager::MODULE_PERMISSION_NS . $module->getName())) {
$menu->mergeSubMenus($module->getMenuItems()); $menu->mergeSubMenus($module->getMenuItems());
} }
}
return $menu->order(); return $menu->order();
} }

View File

@ -70,13 +70,13 @@ class Dashboard extends AbstractWidget
{ {
$manager = Icinga::app()->getModuleManager(); $manager = Icinga::app()->getModuleManager();
foreach ($manager->getLoadedModules() as $module) { foreach ($manager->getLoadedModules() as $module) {
/** @var $module \Icinga\Application\Modules\Module */ if ($this->getUser()->can($manager::MODULE_PERMISSION_NS . $module->getName())) {
$this->mergePanes($module->getPaneItems()); $this->mergePanes($module->getPaneItems());
}
} }
if ($this->user !== null) {
$this->loadUserDashboards(); $this->loadUserDashboards();
}
return $this; return $this;
} }
@ -90,11 +90,11 @@ class Dashboard extends AbstractWidget
{ {
$output = array(); $output = array();
foreach ($this->panes as $pane) { foreach ($this->panes as $pane) {
if ($pane->isUserWidget() === true) { if ($pane->isUserWidget()) {
$output[$pane->getName()] = $pane->toArray(); $output[$pane->getName()] = $pane->toArray();
} }
foreach ($pane->getDashlets() as $dashlet) { foreach ($pane->getDashlets() as $dashlet) {
if ($dashlet->isUserWidget() === true) { if ($dashlet->isUserWidget()) {
$output[$pane->getName() . '.' . $dashlet->getTitle()] = $dashlet->toArray(); $output[$pane->getName() . '.' . $dashlet->getTitle()] = $dashlet->toArray();
} }
} }

View File

@ -12,6 +12,11 @@ use Icinga\Web\Url;
*/ */
class SearchDashboard extends Dashboard class SearchDashboard extends Dashboard
{ {
/**
* Name for the search pane
*
* @var string
*/
const SEARCH_PANE = 'search'; const SEARCH_PANE = 'search';
/** /**
@ -19,13 +24,39 @@ class SearchDashboard extends Dashboard
* *
* @param string $searchString * @param string $searchString
* *
* @return Dashboard|SearchDashboard * @return $this
*/ */
public static function search($searchString = '') public function search($searchString = '')
{ {
$dashboard = new static('searchDashboard'); $pane = $this->createPane(self::SEARCH_PANE)->getPane(self::SEARCH_PANE)->setTitle(t('Search'));
$dashboard->loadSearchDashlets($searchString); $this->activate(self::SEARCH_PANE);
return $dashboard;
$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(); 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 * Compare search URLs based on their priority
* *

View File

@ -38,17 +38,11 @@ class Doc_IcingawebController extends DocController
/** /**
* View a chapter of Icinga Web 2's documentation * 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() public function chapterAction()
{ {
$chapter = $this->getParam('chapter'); $chapter = $this->params->getRequired('chapter');
if ($chapter === null) {
throw new Zend_Controller_Action_Exception(
sprintf($this->translate('Missing parameter %s'), 'chapter'),
404
);
}
$this->renderChapter( $this->renderChapter(
$this->getPath(), $this->getPath(),
$chapter, $chapter,

View File

@ -12,14 +12,13 @@ class Doc_ModuleController extends DocController
* *
* @param string $module The name of the module * @param string $module The name of the module
* @param string $default The default path * @param string $default The default path
* @param bool $suppressErrors Whether to not throw an exception if the module documentation is not * @param bool $suppressErrors Whether to not throw an exception if the module documentation is not available
* available
* *
* @return string|null Path to the documentation or null if the module documentation is not * @return string|null Path to the documentation or null if the module documentation is not available
* available and errors are suppressed * and errors are suppressed
* *
* @throws Zend_Controller_Action_Exception If the module documentation is not available and errors are not * @throws \Icinga\Exception\Http\HttpNotFoundException If the module documentation is not available and errors
* suppressed * are not suppressed
*/ */
protected function getPath($module, $default, $suppressErrors = false) protected function getPath($module, $default, $suppressErrors = false)
{ {
@ -35,10 +34,7 @@ class Doc_ModuleController extends DocController
if ($suppressErrors) { if ($suppressErrors) {
return null; return null;
} }
throw new Zend_Controller_Action_Exception( $this->httpNotFound($this->translate('Documentation for module \'%s\' is not available'), $module);
sprintf($this->translate('Documentation for module \'%s\' is not available'), $module),
404
);
} }
/** /**
@ -48,55 +44,41 @@ class Doc_ModuleController extends DocController
{ {
$moduleManager = Icinga::app()->getModuleManager(); $moduleManager = Icinga::app()->getModuleManager();
$modules = array(); $modules = array();
foreach ($moduleManager->listEnabledModules() as $module) { foreach ($moduleManager->listInstalledModules() as $module) {
$path = $this->getPath($module, $moduleManager->getModuleDir($module, '/doc'), true); $path = $this->getPath($module, $moduleManager->getModuleDir($module, '/doc'), true);
if ($path !== null) { if ($path !== null) {
$modules[] = $moduleManager->getModule($module); $modules[] = $moduleManager->getModule($module, false);
} }
} }
$this->view->modules = $modules; $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 * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
* given module is neither installed nor enabled
*/ */
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(); $moduleManager = Icinga::app()->getModuleManager();
if (! $moduleManager->hasInstalled($moduleName)) { if (! $moduleManager->hasInstalled($moduleName)) {
throw new Zend_Controller_Action_Exception( $this->httpNotFound($this->translate('Module \'%s\' is not installed'), $moduleName);
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
);
} }
} }
/** /**
* View the toc of a module's documentation * 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() public function tocAction()
{ {
$module = $this->getParam('moduleName'); $module = $this->params->getRequired('moduleName');
$this->assertModuleEnabled($module); $this->assertModuleInstalled($module);
$this->view->moduleName = $module; $this->view->moduleName = $module;
try { try {
$this->renderToc( $this->renderToc(
@ -106,28 +88,23 @@ class Doc_ModuleController extends DocController
array('moduleName' => $module) array('moduleName' => $module)
); );
} catch (DocException $e) { } catch (DocException $e) {
throw new Zend_Controller_Action_Exception($e->getMessage(), 404); $this->httpNotFound($e->getMessage());
} }
} }
/** /**
* View a chapter of a module's documentation * 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 * @throws \Icinga\Exception\MissingParameterException If one of the required parameters 'moduleName' and
* the documentation module's library occurs * 'chapter' is empty
* @see assertModuleEnabled() * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
* @see assertModuleInstalled()
*/ */
public function chapterAction() public function chapterAction()
{ {
$module = $this->getParam('moduleName'); $module = $this->params->getRequired('moduleName');
$this->assertModuleEnabled($module); $this->assertModuleInstalled($module);
$chapter = $this->getParam('chapter'); $chapter = $this->params->getRequired('chapter');
if ($chapter === null) {
throw new Zend_Controller_Action_Exception(
sprintf($this->translate('Missing parameter %s'), 'chapter'),
404
);
}
$this->view->moduleName = $module; $this->view->moduleName = $module;
try { try {
$this->renderChapter( $this->renderChapter(
@ -137,19 +114,21 @@ class Doc_ModuleController extends DocController
array('moduleName' => $module) array('moduleName' => $module)
); );
} catch (DocException $e) { } catch (DocException $e) {
throw new Zend_Controller_Action_Exception($e->getMessage(), 404); $this->httpNotFound($e->getMessage());
} }
} }
/** /**
* View a module's documentation as PDF * 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() public function pdfAction()
{ {
$module = $this->getParam('moduleName'); $module = $this->params->getRequired('moduleName');
$this->assertModuleEnabled($module); $this->assertModuleInstalled($module);
$this->renderPdf( $this->renderPdf(
$this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')), $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
$module, $module,

View File

@ -92,9 +92,6 @@ class Doc_SearchController extends DocController
return $path; return $path;
} }
} }
throw new Zend_Controller_Action_Exception( $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available'));
$this->translate('Documentation for Icinga Web 2 is not available'),
404
);
} }
} }

View File

@ -14,7 +14,7 @@
$this->translate('Module documentations'), $this->translate('Module documentations'),
'doc/module/', 'doc/module/',
null, 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'))
); ?></li> ); ?></li>
</ul> </ul>
</div> </div>

View File

@ -4,7 +4,7 @@
</div> </div>
<div class="content"> <div class="content">
<ul> <ul>
<?php foreach ($modules as $module): ?> <?php foreach ($modules as $module): /** @var \Icinga\Application\Modules\Module $module */ ?>
<li><?= $this->qlink( <li><?= $this->qlink(
$module->getTitle(), $module->getTitle(),
'doc/module/toc', 'doc/module/toc',

View File

@ -8,11 +8,11 @@ namespace Tests\Icinga\Web;
require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php'); require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php');
use Mockery; use Mockery;
use Icinga\Application\Icinga; use Icinga\Test\BaseTestCase;
use Icinga\User;
use Icinga\Web\Widget\Dashboard; use Icinga\Web\Widget\Dashboard;
use Icinga\Web\Widget\Dashboard\Pane; use Icinga\Web\Widget\Dashboard\Pane;
use Icinga\Web\Widget\Dashboard\Dashlet; use Icinga\Web\Widget\Dashboard\Dashlet;
use Icinga\Test\BaseTestCase;
class DashletWithMockedView extends Dashlet class DashletWithMockedView extends Dashlet
{ {
@ -52,6 +52,7 @@ class DashboardTest extends BaseTestCase
$moduleMock->shouldReceive('getPaneItems')->andReturn(array( $moduleMock->shouldReceive('getPaneItems')->andReturn(array(
'test-pane' => new Pane('Test Pane') 'test-pane' => new Pane('Test Pane')
)); ));
$moduleMock->shouldReceive('getName')->andReturn('test');
$moduleManagerMock = Mockery::mock('Icinga\Application\Modules\Manager'); $moduleManagerMock = Mockery::mock('Icinga\Application\Modules\Manager');
$moduleManagerMock->shouldReceive('getLoadedModules')->andReturn(array( $moduleManagerMock->shouldReceive('getLoadedModules')->andReturn(array(
@ -130,7 +131,10 @@ class DashboardTest extends BaseTestCase
*/ */
public function testLoadPaneItemsProvidedByEnabledModules() public function testLoadPaneItemsProvidedByEnabledModules()
{ {
$user = new User('test');
$user->setPermissions(array('*' => '*'));
$dashboard = new Dashboard(); $dashboard = new Dashboard();
$dashboard->setUser($user);
$dashboard->load(); $dashboard->load();
$this->assertCount( $this->assertCount(

View File

@ -5,6 +5,7 @@ namespace Tests\Icinga\Web;
use Mockery; use Mockery;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
use Icinga\User;
use Icinga\Web\Widget\SearchDashboard; use Icinga\Web\Widget\SearchDashboard;
class SearchDashboardTest extends BaseTestCase class SearchDashboardTest extends BaseTestCase
@ -19,6 +20,7 @@ class SearchDashboardTest extends BaseTestCase
$moduleMock->shouldReceive('getSearchUrls')->andReturn(array( $moduleMock->shouldReceive('getSearchUrls')->andReturn(array(
$searchUrl $searchUrl
)); ));
$moduleMock->shouldReceive('getName')->andReturn('test');
$moduleManagerMock = Mockery::mock('Icinga\Application\Modules\Manager'); $moduleManagerMock = Mockery::mock('Icinga\Application\Modules\Manager');
$moduleManagerMock->shouldReceive('getLoadedModules')->andReturn(array( $moduleManagerMock->shouldReceive('getLoadedModules')->andReturn(array(
@ -34,14 +36,22 @@ class SearchDashboardTest extends BaseTestCase
*/ */
public function testWhetherRenderThrowsAnExceptionWhenHasNoDashlets() 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->getPane('search')->removeDashlets();
$dashboard->render(); $dashboard->render();
} }
public function testWhetherSearchLoadsSearchDashletsFromModules() 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'); $result = $dashboard->getPane('search')->hasDashlet('Hosts: pending');
@ -50,7 +60,11 @@ class SearchDashboardTest extends BaseTestCase
public function testWhetherSearchProvidesHintWhenSearchStringIsEmpty() 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'); $result = $dashboard->getPane('search')->hasDashlet('Ready to search');