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()
{
$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');

View File

@ -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();

View File

@ -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
);
}
}

View File

@ -1,8 +1,11 @@
<?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">
<input
type="text" name="q" id="search" class="search" placeholder="<?= $this->translate('Search') ?> &hellip;"
@ -13,4 +16,4 @@ use Icinga\Web\Widget\SearchDashboard;
<nav>
<h1 id="navigation" class="sr-only"><?= t('Navigation'); ?></h1>
<?= $menuRenderer; ?>
</nav>
</nav>

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
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/<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

@ -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
<a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/toc#!/icinga2/latest/doc/module/icinga2/chapter/getting-started#setting-up-external-command-pipe">Command Pipe</a>
changing permissions or sending a command to the Icinga instance through the
<a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/toc#!/icinga2/latest/doc/module/icinga2/chapter/getting-started#setting-up-external-command-pipe">Command Pipe</a>
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/<moduleName>` 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/<moduleName> | Allow access to module <moduleName>
#### 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).
## <a id="restrictions"></a> 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

View File

@ -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
);
}
/**

View File

@ -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)
{

View File

@ -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
*

View File

@ -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());
}
/**

View File

@ -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);
}
}

View File

@ -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);
}
}
/**

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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
*

View File

@ -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,

View File

@ -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,

View File

@ -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'));
}
}

View File

@ -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'))
); ?></li>
</ul>
</div>

View File

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

View File

@ -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(

View File

@ -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');