diff --git a/.gitattributes b/.gitattributes index a05c2f5cd..6ae8ee7af 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,3 +6,6 @@ Vagrantfile export-ignore # Normalize puppet manifests' line endings to LF on checkin and prevent conversion to CRLF when the files are checked out .puppet* eol=lf + +# Include version information on `git archive' +/application/VERSION export-subst diff --git a/AUTHORS b/AUTHORS index 8a0bc91d0..f56d6e24d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,6 +18,7 @@ Marius Hein Markus Frosch Matthias Jentsch Michael Friedrich +Paul Richards Rene Moser Susanne Vestner-Ludwig Sylph Lin diff --git a/application/VERSION b/application/VERSION new file mode 100644 index 000000000..519b667a7 --- /dev/null +++ b/application/VERSION @@ -0,0 +1 @@ +$Format:%H%d %ci$ diff --git a/application/controllers/AboutController.php b/application/controllers/AboutController.php new file mode 100644 index 000000000..a4247f234 --- /dev/null +++ b/application/controllers/AboutController.php @@ -0,0 +1,15 @@ +view->version = Version::get(); + } +} diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index 8f2c33e62..a4161981a 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -28,6 +28,10 @@ class GroupController extends AuthBackendController function ($b) { return $b->getName(); }, $this->loadUserGroupBackends('Icinga\Data\Selectable') ); + if (empty($backendNames)) { + return; + } + $this->view->backendSelection = new Form(); $this->view->backendSelection->setAttrib('class', 'backend-selection'); $this->view->backendSelection->setUidDisabled(); @@ -100,6 +104,7 @@ class GroupController extends AuthBackendController $filterEditor = Widget::create('filterEditor') ->setQuery($members) + ->setSearchColumns(array('user')) ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'group') ->ignoreParams('page') ->handleRequest($this->getRequest()); diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php index e944af52c..54482dd7d 100644 --- a/application/controllers/UserController.php +++ b/application/controllers/UserController.php @@ -28,6 +28,10 @@ class UserController extends AuthBackendController function ($b) { return $b->getName(); }, $this->loadUserBackends('Icinga\Data\Selectable') ); + if (empty($backendNames)) { + return; + } + $this->view->backendSelection = new Form(); $this->view->backendSelection->setAttrib('class', 'backend-selection'); $this->view->backendSelection->setUidDisabled(); @@ -99,6 +103,7 @@ class UserController extends AuthBackendController $filterEditor = Widget::create('filterEditor') ->setQuery($memberships) + ->setSearchColumns(array('group_name')) ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'user') ->ignoreParams('page') ->handleRequest($this->getRequest()); diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php index cdb6826be..1a3c2f46e 100644 --- a/application/controllers/UsergroupbackendController.php +++ b/application/controllers/UsergroupbackendController.php @@ -52,7 +52,7 @@ class UsergroupbackendController extends Controller $form->setIniConfig(Config::app('groups')); $form->setOnSuccess(function (UserGroupBackendForm $form) { try { - $form->add($form->getValues()); + $form->add(array_filter($form->getValues())); } catch (Exception $e) { $form->error($e->getMessage()); return false; @@ -85,7 +85,12 @@ class UsergroupbackendController extends Controller $form->setIniConfig(Config::app('groups')); $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) { try { - $form->edit($backendName, $form->getValues()); + $form->edit($backendName, array_map( + function ($v) { + return $v !== '' ? $v : null; + }, + $form->getValues() + )); } catch (Exception $e) { $form->error($e->getMessage()); return false; diff --git a/application/forms/Config/Resource/SshResourceForm.php b/application/forms/Config/Resource/SshResourceForm.php new file mode 100644 index 000000000..c5c49d1e0 --- /dev/null +++ b/application/forms/Config/Resource/SshResourceForm.php @@ -0,0 +1,147 @@ +setName('form_config_resource_ssh'); + } + + /** + * @see Form::createElements() + */ + public function createElements(array $formData) + { + $this->addElement( + 'text', + 'name', + array( + 'required' => true, + 'label' => $this->translate('Resource Name'), + 'description' => $this->translate('The unique name of this resource') + ) + ); + $this->addElement( + 'text', + 'user', + array( + 'required' => true, + 'label' => $this->translate('User'), + 'description' => $this->translate( + 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be' + . ' possible for this user' + ) + ) + ); + + if ($this->getRequest()->getActionName() != 'editresource') { + + $callbackValidator = new Zend_Validate_Callback(function ($value) { + if (openssl_pkey_get_private($value) === false) { + return false; + } + return true; + }); + $callbackValidator->setMessage( + $this->translate('The given SSH key is invalid'), + Zend_Validate_Callback::INVALID_VALUE + ); + + $this->addElement( + 'textarea', + 'private_key', + array( + 'required' => true, + 'label' => $this->translate('Private Key'), + 'description' => $this->translate('The private key which will be used for the SSH connections'), + 'class' => 'resource ssh-identity', + 'validators' => array($callbackValidator) + ) + ); + } else { + $resourceName = $formData['name']; + $this->addElement( + 'note', + 'private_key_note', + array( + 'escape' => false, + 'label' => $this->translate('Private Key'), + 'value' => sprintf( + '%3$s', + $this->getView()->url('config/removeresource', array('resource' => $resourceName)), + sprintf($this->translate( + 'Remove the %s resource' + ), $resourceName), + $this->translate('To modify the private key you must recreate this resource.') + ) + ) + ); + } + + return $this; + } + + /** + * Remove the assigned key to the resource + * + * @param ConfigObject $config + * + * @return bool + */ + public static function beforeRemove(ConfigObject $config) + { + $file = $config->private_key; + + if (file_exists($file)) { + unlink($file); + return true; + } + return false; + } + + /** + * Creates the assigned key to the resource + * + * @param ResourceConfigForm $form + * + * @return bool + */ + public static function beforeAdd(ResourceConfigForm $form) + { + $configDir = Icinga::app()->getConfigDir(); + $user = $form->getElement('user')->getValue(); + + $filePath = $configDir . '/ssh/' . $user; + + if (! file_exists($filePath)) { + $file = File::create($filePath, 0600); + } else { + $form->error( + sprintf($form->translate('The private key for the user "%s" is already exists.'), $user) + ); + return false; + } + + $file->fwrite($form->getElement('private_key')->getValue()); + + $form->getElement('private_key')->setValue($configDir . '/ssh/' . $user); + + return true; + } +} diff --git a/application/forms/Config/ResourceConfigForm.php b/application/forms/Config/ResourceConfigForm.php index bfec65938..5d6626d02 100644 --- a/application/forms/Config/ResourceConfigForm.php +++ b/application/forms/Config/ResourceConfigForm.php @@ -10,6 +10,7 @@ use Icinga\Forms\Config\Resource\DbResourceForm; use Icinga\Forms\Config\Resource\FileResourceForm; use Icinga\Forms\Config\Resource\LdapResourceForm; use Icinga\Forms\Config\Resource\LivestatusResourceForm; +use Icinga\Forms\Config\Resource\SshResourceForm; use Icinga\Application\Platform; use Icinga\Exception\ConfigurationError; @@ -41,6 +42,8 @@ class ResourceConfigForm extends ConfigForm return new LivestatusResourceForm(); } elseif ($type === 'file') { return new FileResourceForm(); + } elseif ($type === 'ssh') { + return new SshResourceForm(); } else { throw new InvalidArgumentException(sprintf($this->translate('Invalid resource type "%s" provided'), $type)); } @@ -55,7 +58,7 @@ class ResourceConfigForm extends ConfigForm * * @return $this * - * @thrwos InvalidArgumentException In case the resource does already exist + * @throws InvalidArgumentException In case the resource does already exist */ public function add(array $values) { @@ -116,6 +119,11 @@ class ResourceConfigForm extends ConfigForm } $resourceConfig = $this->config->getSection($name); + $resourceForm = $this->getResourceForm($resourceConfig->type); + if (method_exists($resourceForm, 'beforeRemove')) { + $resourceForm::beforeRemove($resourceConfig); + } + $this->config->removeSection($name); return $resourceConfig; } @@ -130,8 +138,9 @@ class ResourceConfigForm extends ConfigForm */ public function onSuccess() { + $resourceForm = $this->getResourceForm($this->getElement('type')->getValue()); + if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) { - $resourceForm = $this->getResourceForm($this->getElement('type')->getValue()); if (method_exists($resourceForm, 'isValidResource') && false === $resourceForm::isValidResource($this)) { $this->addElement($this->getForceCreationCheckbox()); return false; @@ -141,6 +150,11 @@ class ResourceConfigForm extends ConfigForm $resource = $this->request->getQuery('resource'); try { if ($resource === null) { // create new resource + if (method_exists($resourceForm, 'beforeAdd')) { + if (! $resourceForm::beforeAdd($this)) { + return false; + } + } $this->add($this->getValues()); $message = $this->translate('Resource "%s" has been successfully created'); } else { // edit existing resource @@ -212,6 +226,7 @@ class ResourceConfigForm extends ConfigForm $resourceTypes = array( 'file' => $this->translate('File'), 'livestatus' => 'Livestatus', + 'ssh' => $this->translate('SSH Identity'), ); if ($resourceType === 'ldap' || Platform::extensionLoaded('ldap')) { $resourceTypes['ldap'] = 'LDAP'; diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php index c520ec68f..ac2091398 100644 --- a/application/forms/Config/UserBackend/LdapBackendForm.php +++ b/application/forms/Config/UserBackend/LdapBackendForm.php @@ -8,7 +8,7 @@ use Icinga\Web\Form; use Icinga\Data\ConfigObject; use Icinga\Data\ResourceFactory; use Icinga\Exception\AuthenticationException; -use Icinga\Authentication\User\LdapUserBackend; +use Icinga\Authentication\User\UserBackend; /** * Form class for adding/modifying LDAP user backends @@ -48,6 +48,8 @@ class LdapBackendForm extends Form */ public function createElements(array $formData) { + $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false; + $this->addElement( 'text', 'name', @@ -77,10 +79,13 @@ class LdapBackendForm extends Form 'text', 'user_class', array( - 'required' => true, - 'label' => $this->translate('LDAP User Object Class'), - 'description' => $this->translate('The object class used for storing users on the LDAP server.'), - 'value' => 'inetOrgPerson' + 'preserveDefault' => true, + 'required' => ! $isAd, + 'ignore' => $isAd, + 'disabled' => $isAd ?: null, + 'label' => $this->translate('LDAP User Object Class'), + 'description' => $this->translate('The object class used for storing users on the LDAP server.'), + 'value' => $isAd ? 'user' : 'inetOrgPerson' ) ); $this->addElement( @@ -117,12 +122,15 @@ class LdapBackendForm extends Form 'text', 'user_name_attribute', array( - 'required' => true, - 'label' => $this->translate('LDAP User Name Attribute'), - 'description' => $this->translate( + 'preserveDefault' => true, + 'required' => ! $isAd, + 'ignore' => $isAd, + 'disabled' => $isAd ?: null, + 'label' => $this->translate('LDAP User Name Attribute'), + 'description' => $this->translate( 'The attribute name used for storing the user name on the LDAP server.' ), - 'value' => 'uid' + 'value' => $isAd ? 'sAMAccountName' : 'uid' ) ); $this->addElement( @@ -130,7 +138,7 @@ class LdapBackendForm extends Form 'backend', array( 'disabled' => true, - 'value' => 'ldap' + 'value' => $isAd ? 'msldap' : 'ldap' ) ); $this->addElement( @@ -170,8 +178,7 @@ class LdapBackendForm extends Form public static function isValidUserBackend(Form $form) { try { - $ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig())); - $ldapUserBackend->setConfig(new ConfigObject($form->getValues())); + $ldapUserBackend = UserBackend::create(null, new ConfigObject($form->getValues())); $ldapUserBackend->assertAuthenticationPossible(); } catch (AuthenticationException $e) { if (($previous = $e->getPrevious()) !== null) { @@ -193,6 +200,8 @@ class LdapBackendForm extends Form * Return the configuration for the chosen resource * * @return ConfigObject + * + * @todo Check whether it's possible to drop this (Or even all occurences!) */ public function getResourceConfig() { diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php index 0a30dd590..62a68e70b 100644 --- a/application/forms/Config/UserBackendConfigForm.php +++ b/application/forms/Config/UserBackendConfigForm.php @@ -60,16 +60,24 @@ class UserBackendConfigForm extends ConfigForm */ public function getBackendForm($type) { - if ($type === 'db') { - $form = new DbBackendForm(); - $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array()); - } elseif ($type === 'ldap') { - $form = new LdapBackendForm(); - $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array()); - } elseif ($type === 'external') { - $form = new ExternalBackendForm(); - } else { - throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type)); + switch ($type) + { + case 'db': + $form = new DbBackendForm(); + $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array()); + break; + case 'ldap': + case 'msldap': + $form = new LdapBackendForm(); + $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array()); + break; + case 'external': + $form = new ExternalBackendForm(); + break; + default: + throw new InvalidArgumentException( + sprintf($this->translate('Invalid backend type "%s" provided'), $type) + ); } return $form; @@ -296,6 +304,7 @@ class UserBackendConfigForm extends ConfigForm } if (isset($this->resources['ldap']) && ($backendType === 'ldap' || Platform::extensionLoaded('ldap'))) { $backendTypes['ldap'] = 'LDAP'; + $backendTypes['msldap'] = 'ActiveDirectory'; } $externalBackends = array_filter( diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php new file mode 100644 index 000000000..c3c8baacf --- /dev/null +++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php @@ -0,0 +1,310 @@ +setName('form_config_ldapusergroupbackend'); + } + + /** + * Create and add elements to this form + * + * @param array $formData + */ + public function createElements(array $formData) + { + $resourceNames = $this->getLdapResourceNames(); + $this->addElement( + 'select', + 'resource', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('LDAP Connection'), + 'description' => $this->translate('The LDAP connection to use for this backend.'), + 'multiOptions' => array_combine($resourceNames, $resourceNames) + ) + ); + $resource = ResourceFactory::create( + isset($formData['resource']) && in_array($formData['resource'], $resourceNames) + ? $formData['resource'] + : $resourceNames[0] + ); + + $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend')); + $userBackendNames = $this->getLdapUserBackendNames($resource); + if (! empty($userBackendNames)) { + $userBackends = array_merge($userBackends, array_combine($userBackendNames, $userBackendNames)); + } + $this->addElement( + 'select', + 'user_backend', + array( + 'required' => true, + 'autosubmit' => true, + 'label' => $this->translate('User Backend'), + 'description' => $this->translate('The user backend to link with this user group backend.'), + 'multiOptions' => $userBackends + ) + ); + + $groupBackend = new LdapUserGroupBackend($resource); + if ($formData['type'] === 'ldap') { + $defaults = $groupBackend->getOpenLdapDefaults(); + $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false! + } else { // $formData['type'] === 'msldap' + $defaults = $groupBackend->getActiveDirectoryDefaults(); + $groupConfigDisabled = $userConfigDisabled = true; + } + + $dnDisabled = null; // MUST BE null + if (isset($formData['user_backend']) && $formData['user_backend'] !== 'none') { + $userBackend = UserBackend::create($formData['user_backend']); + $defaults->merge(array( + 'user_base_dn' => $userBackend->getBaseDn(), + 'user_class' => $userBackend->getUserClass(), + 'user_name_attribute' => $userBackend->getUserNameAttribute(), + 'user_filter' => $userBackend->getFilter() + )); + $userConfigDisabled = $dnDisabled = true; + } + + $this->createGroupConfigElements($defaults, $groupConfigDisabled); + $this->createUserConfigElements($defaults, $userConfigDisabled, $dnDisabled); + } + + /** + * Create and add all elements to this form required for the group configuration + * + * @param ConfigObject $defaults + * @param null|bool $disabled + */ + protected function createGroupConfigElements(ConfigObject $defaults, $disabled) + { + $this->addElement( + 'text', + 'group_class', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP Group Object Class'), + 'description' => $this->translate('The object class used for storing groups on the LDAP server.'), + 'value' => $defaults->group_class + ) + ); + $this->addElement( + 'text', + 'group_filter', + array( + 'preserveDefault' => true, + 'allowEmpty' => true, + 'label' => $this->translate('LDAP Group Filter'), + 'description' => $this->translate( + 'An additional filter to use when looking up groups using the specified connection. ' + . 'Leave empty to not to use any additional filter rules.' + ), + 'requirement' => $this->translate( + 'The filter needs to be expressed as standard LDAP expression, without' + . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)' + ), + 'validators' => array( + array( + 'Callback', + false, + array( + 'callback' => function ($v) { + return strpos($v, '(') !== 0; + }, + 'messages' => array( + 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.') + ) + ) + ) + ), + 'value' => $defaults->group_filter + ) + ); + $this->addElement( + 'text', + 'group_name_attribute', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP Group Name Attribute'), + 'description' => $this->translate( + 'The attribute name used for storing a group\'s name on the LDAP server.' + ), + 'value' => $defaults->group_name_attribute + ) + ); + $this->addElement( + 'text', + 'base_dn', + array( + 'preserveDefault' => true, + 'label' => $this->translate('LDAP Group Base DN'), + 'description' => $this->translate( + 'The path where groups can be found on the LDAP server. Leave ' . + 'empty to select all users available using the specified connection.' + ), + 'value' => $defaults->base_dn + ) + ); + } + + /** + * Create and add all elements to this form required for the user configuration + * + * @param ConfigObject $defaults + * @param null|bool $disabled + * @param null|bool $dnDisabled + */ + protected function createUserConfigElements(ConfigObject $defaults, $disabled, $dnDisabled) + { + $this->addElement( + 'text', + 'user_class', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP User Object Class'), + 'description' => $this->translate('The object class used for storing users on the LDAP server.'), + 'value' => $defaults->user_class + ) + ); + $this->addElement( + 'text', + 'user_filter', + array( + 'preserveDefault' => true, + 'allowEmpty' => true, + 'ignore' => $dnDisabled, + 'disabled' => $dnDisabled, + 'label' => $this->translate('LDAP User Filter'), + 'description' => $this->translate( + 'An additional filter to use when looking up users using the specified connection. ' + . 'Leave empty to not to use any additional filter rules.' + ), + 'requirement' => $this->translate( + 'The filter needs to be expressed as standard LDAP expression, without' + . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)' + ), + 'validators' => array( + array( + 'Callback', + false, + array( + 'callback' => function ($v) { + return strpos($v, '(') !== 0; + }, + 'messages' => array( + 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.') + ) + ) + ) + ), + 'value' => $defaults->user_filter + ) + ); + $this->addElement( + 'text', + 'user_name_attribute', + array( + 'preserveDefault' => true, + 'ignore' => $disabled, + 'disabled' => $disabled, + 'label' => $this->translate('LDAP User Name Attribute'), + 'description' => $this->translate( + 'The attribute name used for storing a user\'s name on the LDAP server.' + ), + 'value' => $defaults->user_name_attribute + ) + ); + $this->addElement( + 'text', + 'user_base_dn', + array( + 'preserveDefault' => true, + 'ignore' => $dnDisabled, + 'disabled' => $dnDisabled, + 'label' => $this->translate('LDAP User Base DN'), + 'description' => $this->translate( + 'The path where users can be found on the LDAP server. Leave ' . + 'empty to select all users available using the specified connection.' + ), + 'value' => $defaults->user_base_dn + ) + ); + } + + /** + * Return the names of all configured LDAP resources + * + * @return array + */ + protected function getLdapResourceNames() + { + $names = array(); + foreach (ResourceFactory::getResourceConfigs() as $name => $config) { + if (in_array(strtolower($config->type), array('ldap', 'msldap'))) { + $names[] = $name; + } + } + + if (empty($names)) { + Notification::error( + $this->translate('No LDAP resources available. Please configure an LDAP resource first.') + ); + $this->getResponse()->redirectAndExit('config/createresource'); + } + + return $names; + } + + /** + * Return the names of all configured LDAP user backends + * + * @param Connection $resource + * + * @return array + */ + protected function getLdapUserBackendNames(Connection $resource) + { + $names = array(); + foreach (Config::app('authentication') as $name => $config) { + if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) { + $backendResource = ResourceFactory::create($config->resource); + if ( + $backendResource->getHostname() === $resource->getHostname() + && $backendResource->getPort() === $resource->getPort() + ) { + $names[] = $name; + } + } + } + + return $names; + } +} diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php index dd9875c9a..5bd0d9681 100644 --- a/application/forms/Config/UserGroup/UserGroupBackendForm.php +++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php @@ -13,6 +13,13 @@ use Icinga\Forms\ConfigForm; */ class UserGroupBackendForm extends ConfigForm { + /** + * The backend to load when displaying the form for the first time + * + * @var string + */ + protected $backendToLoad; + /** * Initialize this form */ @@ -31,10 +38,17 @@ class UserGroupBackendForm extends ConfigForm */ public function getBackendForm($type) { - if ($type === 'db') { - return new DbUserGroupBackendForm(); - } else { - throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type)); + switch ($type) + { + case 'db': + return new DbUserGroupBackendForm(); + case 'ldap': + case 'msldap': + return new LdapUserGroupBackendForm(); + default: + throw new InvalidArgumentException( + sprintf($this->translate('Invalid backend type "%s" provided'), $type) + ); } } @@ -53,10 +67,7 @@ class UserGroupBackendForm extends ConfigForm throw new NotFoundError('No user group backend called "%s" found', $name); } - $data = $this->config->getSection($name)->toArray(); - $data['type'] = $data['backend']; - $data['name'] = $name; - $this->populate($data); + $this->backendToLoad = $name; return $this; } @@ -103,13 +114,23 @@ class UserGroupBackendForm extends ConfigForm } $backendConfig = $this->config->getSection($name); - if (isset($data['name']) && $data['name'] !== $name) { - $this->config->removeSection($name); - $name = $data['name']; + if (isset($data['name'])) { + if ($data['name'] !== $name) { + $this->config->removeSection($name); + $name = $data['name']; + } + unset($data['name']); } - $this->config->setSection($name, $backendConfig->merge($data)); + $backendConfig->merge($data); + foreach ($backendConfig->toArray() as $k => $v) { + if ($v === null) { + unset($backendConfig->$k); + } + } + + $this->config->setSection($name, $backendConfig); return $this; } @@ -161,7 +182,9 @@ class UserGroupBackendForm extends ConfigForm // TODO(jom): We did not think about how to configure custom group backends yet! $backendTypes = array( - 'db' => $this->translate('Database') + 'db' => $this->translate('Database'), + 'ldap' => 'LDAP', + 'msldap' => 'ActiveDirectory' ); $backendType = isset($formData['type']) ? $formData['type'] : null; @@ -191,8 +214,34 @@ class UserGroupBackendForm extends ConfigForm ) ); - $backendForm = $this->getBackendForm($backendType); - $backendForm->createElements($formData); - $this->addElements($backendForm->getElements()); + $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form'); + } + + /** + * Populate the configuration of the backend to load + */ + public function onRequest() + { + if ($this->backendToLoad) { + $data = $this->config->getSection($this->backendToLoad)->toArray(); + $data['type'] = $data['backend']; + $data['name'] = $this->backendToLoad; + $this->populate($data); + } + } + + /** + * Retrieve all form element values + * + * @param bool $suppressArrayNotation Ignored + * + * @return array + */ + public function getValues($suppressArrayNotation = false) + { + $values = parent::getValues(); + $values = array_merge($values, $values['backend_form']); + unset($values['backend_form']); + return $values; } } diff --git a/application/views/scripts/about/index.phtml b/application/views/scripts/about/index.phtml new file mode 100644 index 000000000..e270a8eb9 --- /dev/null +++ b/application/views/scripts/about/index.phtml @@ -0,0 +1,25 @@ +
+

Icinga Web 2

+ $this->translate('Version: %s'), + 'gitCommitID' => $this->translate('Git commit ID: %s'), + 'gitCommitDate' => $this->translate('Git commit date: %s') + ) as $key => $label) { + if (array_key_exists($key, $version) && null !== ($value = $version[$key])) { + $versionInfo[] = sprintf($label, htmlspecialchars($value)); + } + } + } + + echo ( + 0 === count($versionInfo) + ? '

' . $this->translate( + 'Can\'t determine Icinga Web 2\'s version' + ) + : '

' . nl2br(implode("\n", $versionInfo), false) + ) . '

'; + ?> +
diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml index bcd9dca93..65c1591cd 100644 --- a/application/views/scripts/group/list.phtml +++ b/application/views/scripts/group/list.phtml @@ -18,7 +18,7 @@ if (! $this->compact): ?>
translate('No backend found which is able to list groups') . '
'; return; } else { diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml index 636a449d4..20538a9b3 100644 --- a/application/views/scripts/group/show.phtml +++ b/application/views/scripts/group/show.phtml @@ -31,15 +31,16 @@ if ($this->hasPermission('config/authentication/groups/edit') && $backend instan

escape($group->group_name); ?>

translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($group->created_at); ?>

translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?>

+

translate('Members'); ?>

+ compact): ?> + sortBox; ?> + + limiter; ?> + paginator; ?> + compact): ?> + filterEditor; ?> + - compact): ?> - sortBox; ?> - - limiter; ?> - paginator; ?> - compact): ?> - filterEditor; ?> -
0): ?> diff --git a/application/views/scripts/role/list.phtml b/application/views/scripts/role/list.phtml index 766ba26f3..568bbdf98 100644 --- a/application/views/scripts/role/list.phtml +++ b/application/views/scripts/role/list.phtml @@ -67,7 +67,7 @@ - + translate('Create a New Role') ?>
diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml index 76a6f2b8b..df9a14a45 100644 --- a/application/views/scripts/user/list.phtml +++ b/application/views/scripts/user/list.phtml @@ -18,7 +18,7 @@ if (! $this->compact): ?>
translate('No backend found which is able to list users') . '
'; return; } else { diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml index 82f4c53f9..64a9eaa02 100644 --- a/application/views/scripts/user/show.phtml +++ b/application/views/scripts/user/show.phtml @@ -31,15 +31,16 @@ if ($this->hasPermission('config/authentication/users/edit') && $backend instanc

translate('State'); ?>: is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?>

translate('Created at'); ?>: created_at === null ? '-' : $this->formatDateTime($user->created_at); ?>

translate('Last modified'); ?>: last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?>

+

translate('Group Memberships'); ?>

+ compact): ?> + sortBox; ?> + + limiter; ?> + paginator; ?> + compact): ?> + filterEditor; ?> + - compact): ?> - sortBox; ?> - - limiter; ?> - paginator; ?> - compact): ?> - filterEditor; ?> -
0): ?> diff --git a/doc/about.md b/doc/about.md new file mode 100644 index 000000000..48b4041ac --- /dev/null +++ b/doc/about.md @@ -0,0 +1,70 @@ +# About Icinga Web 2 + +Icinga Web 2 is a powerful PHP framework for web applications that comes in a clean and reduced design. +It's fast, responsive, accessible and easily extensible with modules. + +## The monitoring module + +This is the core module for most Icinga Web 2 users. + +It provides an intuitive user interface for monitoring with Icinga (1 and 2). +Especially there are lots of list and detail views (e.g. for hosts and services) +you can sort and filter depending on what you want to see. + +You can also control the monitoring process itself by sending external commands to Icinga. +Most such actions (like rescheduling a check) can be done with just a single click. + +## Installation + +Icinga Web 2 can be installed easily from packages from the official package repositories. +Setting it up is also easy with the web based setup wizard. + +See [here](installation#installation) for more information about the installation. + +## Configuration + +Icinga Web 2 can be configured via the user interface and .ini files. + +See [here](configuration#configuration) for more information about the configuration. + +## Authentication + +With Icinga Web 2 you can authenticate against relational databases, LDAP and more. +These authentication methods can be easily configured (via the corresponding .ini file). + +See [here](authentication#authentication) for more information about +the different authentication methods available and how to configure them. + +## Authorization + +In Icinga Web 2 there are permissions and restrictions to allow and deny (respectively) +roles to view or to do certain things. +These roles can be assigned to users and groups. + +See [here](security#security) for more information about authorization +and how to configure roles. + +## User preferences + +Besides the global configuration each user has individual configuration options +like the interface's language or the current timezone. +They can be stored either in a database or in .ini files. + +See [here](preferences#preferences) for more information about a user's preferences +and how to configure their storage type. + +## Documentation + +With the documentation module you can read the documentation of the framework (and any module) directly in the user interface. + +The module can also export the documentation to PDF. + +## Translation + +With the translation module every piece of text in the user interface (of the framework itself and any module) can be translated to a language of your choice. + +Currently provided languages: + +* German +* Italian +* Portuguese diff --git a/doc/resources.md b/doc/resources.md index 29a35c30d..a2bfb66af 100644 --- a/doc/resources.md +++ b/doc/resources.md @@ -8,7 +8,7 @@ different files, when the information about a data source changes. Each section in **config/resources.ini** represents a data source with the section name being the identifier used to reference this specific data source. Depending on the data source type, the sections define different directives. -The available data source types are *db*, *ldap* and *livestatus* which will described in detail in the following +The available data source types are *db*, *ldap*, *ssh* and *livestatus* which will described in detail in the following paragraphs. ### Database @@ -64,6 +64,26 @@ bind_dn = "cn=admin,ou=people,dc=icinga,dc=org" bind_pw = admin` ```` +### SSH + +A SSH resource contains the information about the user and the private key location, which can be used for the key-based +ssh authentication. + +Directive | Description +--------------------|------------ +**type** | `ssh` +**user** | The username to use when connecting to the server. +**private_key** | The path to the private key of the user. + +**Example:** + +```` +[ssh] +type = "ssh" +user = "ssh-user" +private_key = "/etc/icingaweb2/ssh/ssh-user" +```` + ### Livestatus A Livestatus resource represents the location of a Livestatus socket which is used for fetching monitoring data. diff --git a/doc/security.md b/doc/security.md new file mode 100644 index 000000000..f975c5c9b --- /dev/null +++ b/doc/security.md @@ -0,0 +1,279 @@ +# Security + +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 +environment they are in charge of. + +This chapter will describe how to do the security configuration of Icinga Web 2 +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 +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 +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 +allowed to do. Permissions are described in greater detail in the +section [Permissions](#permissions). + +### Objects + +There are all kinds of different objects in Icinga Web 2: Hosts, Services, Notifications, Downtimes and Events. + +By default, a user can **see everything**, but it is possible to **explicitly restrict** what each user can see using restrictions. + +Restrictions are complex filter queries that describe what objects should be displayed to a user. Restrictions are described +in greater detail in the section [Restrictions](#restrictions). + +### Users + +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. +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 +an **authentication backend**. For extended information on setting up authentication backends and managing users, please read the chapter [Authentication](authentication.md#authentication). + + +
+ Since Icinga Web 2, users in the Icinga configuration and the web authentication are separated, to allow + use of external authentication providers. This means that users and groups defined in the Icinga configuration are not available to Icinga Web 2. Instead it uses its own authentication + backend to fetch users and groups from, which must be configured separately. +
+ +#### Managing Users + +When using a [Database +as authentication backend](authentication.md#authentication-configuration-db-authentication), it is possible to create, add and delete users directly in the frontend. This configuration +can be found at **Configuration > Authentication > Users **. + +### Groups + +If there is a big amount of users to manage, it would be tedious to specify each user +separately when regularly referring to the same group of users. Because of that, it is possible to group users. +A user can be member of multiple groups and will inherit all permissions and restrictions. + +Like users, groups are identified solely by their **name** that is provided by + a **group backend**. For extended information on setting up group backends, + please read the chapter [Authentication](authentication.md#authentication). + + +#### Managing Groups + +When using a [Database as an authentication backend](#authentication.md#authentication-configuration-db-authentication), +it is possible to manage groups and group memberships directly in the frontend. This configuration +can be found at **Configuration > Authentication > Groups **. + +## Roles + +A role defines a set of **permissions** and **restrictions** and assigns +those to **users** and **groups**. For example, a role **admins** could define that certain +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 +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). + +### Configuration + +Roles can be changed either through the icingaweb2 interface, by navigation +to the page **Configuration > Authentication > Roles**, or through editing the +configuration file: + + + /etc/icingaweb2/roles.ini + + +#### Introducing Example + +To get you a quick start, here is an example of what a role definition could look like: + + + [winadmin] + users = "jdoe, janedoe" + groups = "admin" + permissions = "config/application/*, monitoring/commands/schedule-check" + monitoring/filter/objects = "host=*win*" + + +This example creates a role called **winadmin**, that grants all permissions in `config/application/*` and `monitoring/commands/schedule-check` and additionally only +allows the hosts and services that match the filter `host=*win*` to be displayed. The users +**jdoe** and **janedoe** and all members of the group **admin** will be affected +by this role. + + +#### Syntax + +Each role is defined as a section, with the name of the role as section name. The following +attributes can be defined for each role in a default Icinga Web 2 installation: + + + 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 + + + +## Permissions + +Permissions can be used to allow users or groups certain **actions**. By default, +all actions are **prohibited** and must be allowed explicitly by a role for any user. + +Each action in Icinga Web 2 is denoted by a **namespaced key**, which is used to order and +group those actions. All actions that affect the configuration of Icinga Web 2, are in a +namespace called **config**, while all configurations that affect authentication +are in the namespace `config/authentication` + +**Wildcards** can be used to grant permission for all actions in a certain namespace. +The permission `config/*` would grant permission to all configuration 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 +through a group) all permissions can simply be added together to get the users actual permission set. + +#### Global permissions + + Name | Permits +-------------------------------------|----------------------------------------------------------------- + * | Allow everything, including module-specific permissions + config/* | Allow all configuration actions + config/application/* | Allow configuring IcingaWeb2 + config/application/general | Allow general settings, like logging or preferences + config/application/resources | Allow changing resources for retrieving data + config/application/userbackend | Allow changing backends for retrieving available users + config/application/usergroupbackend | Allow changing backends for retrieving available groups + config/authentication/* | Allow configuring IcingaWeb2 authentication mechanisms + config/authentication/users/* | Allow all user actions + config/authentication/users/show | Allow displaying avilable users + config/authentication/users/add | Allow adding a new user to the backend + config/authentication/users/edit | Allow editing an existing user in the backend + config/authentication/users/remove | Allow removing an existing user from the backend + config/authentication/groups/* | Allow all group actions + config/authentication/groups/show | Allow displaying all available groups + config/authentication/groups/add | Allow adding a new group to the backend + config/authentication/groups/edit | Allow editing existing groups in a backend + config/authentication/groups/remove | Allow removing existing groups from the backend + config/authentication/roles/* | Allow all role actions + config/authentication/roles/add | Allow adding a new role + config/authentication/roles/show | Allow displaying available roles + config/authentication/roles/edit | Allow changing an existing role + config/authentication/roles/remove | Allow removing an existing row + config/modules | Allow enabling or disabling modules + + +#### 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). + + +## 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 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 +mentioned in the section [Syntax](#syntax). + +### Filter Expressions + +Filters operate on columns. A complete list of all available filter columns on hosts and services can be found in +the [monitoring module documentation](/icingaweb2/doc/module/doc/chapter/monitoring-security#monitoring-security-restrictions). + +Any filter expression that is allowed in the filtered view, is also an allowed filter expression. +This means, that it is possible to define negations, wildcards, and even nested +filter expressions containing AND and OR-Clauses. + +The filter expression will be **implicitly** added as an **AND-Clause** to each query on +the filtered data. The following shows the filter expression `host=*win*` being applied on `monitoring/filter/objects`. + + +Regular filter query: + + AND-- service_problem = 1 + | + +--- service_handled = 0 + + +With our restriction applied, any user affected by this restrictions will see the +results of this query instead: + + + AND-- host = *win* + | + +--AND-- service_problem = 1 + | + +--- service_handled = 0 + + +#### Stacking Filters + +When multiple roles assign restrictions to the same user, either directly or indirectly +through a group, all filters will be combined using an **OR-Clause**, resulting in the final +expression: + + + AND-- OR-- $FILTER1 + | | + | +-- $FILTER2 + | | + | +-- $FILTER3 + | + +--AND-- service_problem = 1 + | + +--- service_handled = 0 + + +As a result, a user is be able to see hosts that are matched by **ANY** of +the filter expressions. The following examples will show the usefulness of this behavior: + +#### Example 1: Negation + + [winadmin] + groups = "windows-admins" + monitoring/filter/objects = "host=*win*" + +Will display only hosts and services whose host name contains **win**. + + [webadmin] + groups = "web-admins" + monitoring/filter/objects = "host!=*win*" + +Will only match hosts and services whose host name does **not** contain **win** + +Notice that because of the behavior of two stacking filters, a user that is member of **windows-admins** and **web-admins**, will now be able to see both, Windows and non-Windows hosts and services. + +#### Example 2: Hostgroups + + [unix-server] + 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 +that are part of the host-group linux-servers or the host-group bsd-servers. \ No newline at end of file diff --git a/icingaweb2.spec b/icingaweb2.spec index 1ea69451d..84c3e0b13 100644 --- a/icingaweb2.spec +++ b/icingaweb2.spec @@ -201,7 +201,7 @@ cp -prv packages/files/config/modules/setup %{buildroot}/%{configdir}/modules/ %pre getent group icingacmd >/dev/null || groupadd -r icingacmd -%if 0%{?suse_version} +%if 0%{?suse_version} && 0%{?suse_version} < 01200 usermod -A icingacmd,%{icingawebgroup} %{wwwuser} %else usermod -a -G icingacmd,%{icingawebgroup} %{wwwuser} @@ -218,6 +218,7 @@ rm -rf %{buildroot} %{basedir}/application/forms %{basedir}/application/layouts %{basedir}/application/views +%{basedir}/application/VERSION %{basedir}/doc %{basedir}/modules %{basedir}/public diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index e265b91ee..6f409789a 100644 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -134,18 +134,20 @@ abstract class ApplicationBootstrap $this->vendorDir = $baseDir . '/library/vendor'; $this->libDir = realpath(__DIR__ . '/../..'); + $this->setupAutoloader(); + if ($configDir === null) { if (array_key_exists('ICINGAWEB_CONFIGDIR', $_SERVER)) { $configDir = $_SERVER['ICINGAWEB_CONFIGDIR']; } else { - $configDir = '/etc/icingaweb2'; + $configDir = Platform::isWindows() + ? $baseDir . '/config' + : '/etc/icingaweb2'; } } $canonical = realpath($configDir); $this->configDir = $canonical ? $canonical : $configDir; - $this->setupAutoloader(); - set_include_path( implode( PATH_SEPARATOR, diff --git a/library/Icinga/Application/Platform.php b/library/Icinga/Application/Platform.php index 355311b97..cc72857f9 100644 --- a/library/Icinga/Application/Platform.php +++ b/library/Icinga/Application/Platform.php @@ -59,6 +59,122 @@ class Platform return strtoupper(substr(self::getOperatingSystemName(), 0, 5)) === 'LINUX'; } + /** + * Return the Linux distribution's name + * or 'linux' if the name could not be found out + * or false if the OS isn't Linux or an error occurred + * + * @param int $reliable + * 3: Only parse /etc/os-release (or /usr/lib/os-release). + * For the paranoid ones. + * 2: If that (3) doesn't help, check /etc/*-release, too. + * If something is unclear, return 'linux'. + * 1: Almost equal to mode 2. The possible return values also include: + * 'redhat' -- unclear whether RHEL/Fedora/... + * 'suse' -- unclear whether SLES/openSUSE/... + * 0: If even that (1) doesn't help, check /proc/version, too. + * This may not work (as expected) on LXC containers! + * (No reliability at all!) + * + * @return string|bool + */ + public static function getLinuxDistro($reliable = 2) + { + if (! self::isLinux()) { + return false; + } + + foreach (array('/etc/os-release', '/usr/lib/os-release') as $osReleaseFile) { + if (false === ($osRelease = @file( + $osReleaseFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES + ))) { + continue; + } + + foreach ($osRelease as $osInfo) { + if (false === ($res = @preg_match('/(? 2) { + return 'linux'; + } + + foreach (array( + 'fedora' => '/etc/fedora-release', + 'centos' => '/etc/centos-release' + ) as $distro => $releaseFile) { + if (! (false === ( + $release = @file_get_contents($releaseFile) + ) || false === strpos(strtolower($release), $distro))) { + return $distro; + } + } + + if (false !== ($release = @file_get_contents('/etc/redhat-release'))) { + $release = strtolower($release); + if (false !== strpos($release, 'red hat enterprise linux')) { + return 'rhel'; + } + foreach (array('fedora', 'centos') as $distro) { + if (false !== strpos($release, $distro)) { + return $distro; + } + } + return $reliable < 2 ? 'redhat' : 'linux'; + } + + if (false !== ($release = @file_get_contents('/etc/SuSE-release'))) { + $release = strtolower($release); + foreach (array( + 'opensuse' => 'opensuse', + 'sles' => 'suse linux enterprise server', + 'sled' => 'suse linux enterprise desktop' + ) as $distro => $name) { + if (false !== strpos($release, $name)) { + return $distro; + } + } + return $reliable < 2 ? 'suse' : 'linux'; + } + + if ($reliable < 1) { + if (false === ($procVersion = @file_get_contents('/proc/version'))) { + return false; + } + $procVersion = strtolower($procVersion); + foreach (array( + 'redhat' => 'red hat', + 'suse' => 'suse linux', + 'ubuntu' => 'ubuntu', + 'debian' => 'debian' + ) as $distro => $name) { + if (false !== strpos($procVersion, $name)) { + return $distro; + } + } + } + + return 'linux'; + } + /** * Test of CLI environment * diff --git a/library/Icinga/Application/Version.php b/library/Icinga/Application/Version.php new file mode 100644 index 000000000..65b6138d9 --- /dev/null +++ b/library/Icinga/Application/Version.php @@ -0,0 +1,37 @@ +getApplicationDir() . DIRECTORY_SEPARATOR . 'VERSION' + ))) { + return false; + } + + $matches = array(); + if (false === ($res = preg_match( + '/(?\w+)(?:\s*\(.*?(?:(?<=[\(,])\s*tag\s*:\s*v(?P.+?)\s*(?=[\),]).*?)?\))?\s*(?P\S+)/ms', + $appVersion, + $matches + )) || $res === 0) { + return false; + } + + foreach ($matches as $key => $value) { + if (is_int($key) || $value === '') { + unset($matches[$key]); + } + } + return $matches; + } +} diff --git a/library/Icinga/Application/webrouter.php b/library/Icinga/Application/webrouter.php index 905fa7dca..5a7d1c95c 100644 --- a/library/Icinga/Application/webrouter.php +++ b/library/Icinga/Application/webrouter.php @@ -31,7 +31,7 @@ $baseDir = $_SERVER['DOCUMENT_ROOT']; $baseDir = dirname($_SERVER['SCRIPT_FILENAME']); // Fix aliases -$remove = dirname($_SERVER['PHP_SELF']); +$remove = str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])); if (substr($ruri, 0, strlen($remove)) !== $remove) { return false; } diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 154a33a44..333969891 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -4,17 +4,16 @@ namespace Icinga\Authentication\User; use DateTime; -use Icinga\Application\Logger; use Icinga\Data\ConfigObject; use Icinga\Exception\AuthenticationException; use Icinga\Exception\ProgrammingError; -use Icinga\Repository\Repository; +use Icinga\Repository\LdapRepository; use Icinga\Repository\RepositoryQuery; use Icinga\Protocol\Ldap\Exception as LdapException; use Icinga\Protocol\Ldap\Expression; use Icinga\User; -class LdapUserBackend extends Repository implements UserBackendInterface +class LdapUserBackend extends LdapRepository implements UserBackendInterface { /** * The base DN to use for a query @@ -65,20 +64,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface ) ); - protected $groupOptions; - - /** - * Normed attribute names based on known LDAP environments - * - * @var array - */ - protected $normedAttributes = array( - 'uid' => 'uid', - 'user' => 'user', - 'inetorgperson' => 'inetOrgPerson', - 'samaccountname' => 'sAMAccountName' - ); - /** * Set the base DN to use for a query * @@ -179,34 +164,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface return $this->filter; } - public function setGroupOptions(array $options) - { - $this->groupOptions = $options; - return $this; - } - - public function getGroupOptions() - { - return $this->groupOptions; - } - - /** - * Return the given attribute name normed to known LDAP enviroments, if possible - * - * @param string $name - * - * @return string - */ - protected function getNormedAttribute($name) - { - $loweredName = strtolower($name); - if (array_key_exists($loweredName, $this->normedAttributes)) { - return $this->normedAttributes[$loweredName]; - } - - return $name; - } - /** * Apply the given configuration to this backend * @@ -325,37 +282,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0; } - /** - * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation - * - * @param string|null $value - * - * @return int - */ - protected function retrieveGeneralizedTime($value) - { - if ($value === null) { - return $value; - } - - if ( - ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false - || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false - ) { - return $dateTime->getTimeStamp(); - } else { - Logger::debug(sprintf( - 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) for user backend "%s".', - $value, - $this->getName() - )); - } - } - /** * Return whether the given shadowExpire value defines that a user is permitted to login * @@ -413,41 +339,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface } } - /** - * Retrieve the user groups - * - * @TODO: Subject to change, see #7343 - * - * @param string $dn - * - * @return array - */ - public function getGroups($dn) - { - if (empty($this->groupOptions) || ! isset($this->groupOptions['group_base_dn'])) { - return array(); - } - - $result = $this->ds->select() - ->setBase($this->groupOptions['group_base_dn']) - ->from( - $this->groupOptions['group_class'], - array($this->groupOptions['group_attribute']) - ) - ->where( - $this->groupOptions['group_member_attribute'], - $dn - ) - ->fetchAll(); - - $groups = array(); - foreach ($result as $group) { - $groups[] = $group->{$this->groupOptions['group_attribute']}; - } - - return $groups; - } - /** * Authenticate the given user * @@ -472,15 +363,7 @@ class LdapUserBackend extends Repository implements UserBackendInterface return false; } - $authenticated = $this->ds->testCredentials($userDn, $password); - if ($authenticated) { - $groups = $this->getGroups($userDn); - if ($groups !== null) { - $user->setGroups($groups); - } - } - - return $authenticated; + return $this->ds->testCredentials($userDn, $password); } catch (LdapException $e) { throw new AuthenticationException( 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php index 3d11289fb..7afd561a7 100644 --- a/library/Icinga/Authentication/User/UserBackend.php +++ b/library/Icinga/Authentication/User/UserBackend.php @@ -3,6 +3,7 @@ namespace Icinga\Authentication\User; +use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Application\Icinga; use Icinga\Data\ConfigObject; @@ -106,8 +107,17 @@ class UserBackend * * @throws ConfigurationError */ - public static function create($name, ConfigObject $backendConfig) + public static function create($name, ConfigObject $backendConfig = null) { + if ($backendConfig === null) { + $authConfig = Config::app('authentication'); + if ($authConfig->hasSection($name)) { + $backendConfig = $authConfig->getSection($name); + } else { + throw new ConfigurationError('User backend "%s" does not exist', $name); + } + } + if ($backendConfig->name !== null) { $name = $backendConfig->name; } @@ -165,12 +175,6 @@ class UserBackend $backend->setUserClass($backendConfig->get('user_class', 'user')); $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName')); $backend->setFilter($backendConfig->filter); - $backend->setGroupOptions(array( - 'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()), - 'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'), - 'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'), - 'group_class' => $backendConfig->get('group_class', 'group') - )); break; case 'ldap': $backend = new LdapUserBackend($resource); @@ -178,12 +182,6 @@ class UserBackend $backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson')); $backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid')); $backend->setFilter($backendConfig->filter); - $backend->setGroupOptions(array( - 'group_base_dn' => $backendConfig->group_base_dn, - 'group_attribute' => $backendConfig->group_attribute, - 'group_member_attribute' => $backendConfig->group_member_attribute, - 'group_class' => $backendConfig->group_class - )); break; } diff --git a/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php new file mode 100644 index 000000000..224013e9f --- /dev/null +++ b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php @@ -0,0 +1,627 @@ + array( + 'order' => 'asc' + ) + ); + + /** + * Normed attribute names based on known LDAP environments + * + * @var array + */ + protected $normedAttributes = array( + 'uid' => 'uid', + 'gid' => 'gid', + 'user' => 'user', + 'group' => 'group', + 'member' => 'member', + 'inetorgperson' => 'inetOrgPerson', + 'samaccountname' => 'sAMAccountName' + ); + + /** + * The name of this repository + * + * @var string + */ + protected $name; + + /** + * The datasource being used + * + * @var Connection + */ + protected $ds; + + /** + * Create a new LDAP repository object + * + * @param Connection $ds The data source to use + */ + public function __construct($ds) + { + $this->ds = $ds; + } + + /** + * Return the given attribute name normed to known LDAP enviroments, if possible + * + * @param string $name + * + * @return string + */ + protected function getNormedAttribute($name) + { + $loweredName = strtolower($name); + if (array_key_exists($loweredName, $this->normedAttributes)) { + return $this->normedAttributes[$loweredName]; + } + + return $name; + } + + /** + * Set this repository's name + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } + + /** + * Return this repository's name + * + * In case no name has been explicitly set yet, the class name is returned. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set the base DN to use for a user query + * + * @param string $baseDn + * + * @return $this + */ + public function setUserBaseDn($baseDn) + { + if (($baseDn = trim($baseDn))) { + $this->userBaseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a user query + * + * @return string + */ + public function getUserBaseDn() + { + return $this->userBaseDn; + } + + /** + * Set the base DN to use for a group query + * + * @param string $baseDn + * + * @return $this + */ + public function setGroupBaseDn($baseDn) + { + if (($baseDn = trim($baseDn))) { + $this->groupBaseDn = $baseDn; + } + + return $this; + } + + /** + * Return the base DN to use for a group query + * + * @return string + */ + public function getGroupBaseDn() + { + return $this->groupBaseDn; + } + + /** + * Set the objectClass where to look for users + * + * @param string $userClass + * + * @return $this + */ + public function setUserClass($userClass) + { + $this->userClass = $this->getNormedAttribute($userClass); + return $this; + } + + /** + * Return the objectClass where to look for users + * + * @return string + */ + public function getUserClass() + { + return $this->userClass; + } + + /** + * Set the objectClass where to look for groups + * + * Sets also the base table name for the underlying repository. + * + * @param string $groupClass + * + * @return $this + */ + public function setGroupClass($groupClass) + { + $this->baseTable = $this->groupClass = $this->getNormedAttribute($groupClass); + return $this; + } + + /** + * Return the objectClass where to look for groups + * + * @return string + */ + public function getGroupClass() + { + return $this->groupClass; + } + + /** + * Set the attribute name where to find a user's name + * + * @param string $userNameAttribute + * + * @return $this + */ + public function setUserNameAttribute($userNameAttribute) + { + $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute); + return $this; + } + + /** + * Return the attribute name where to find a user's name + * + * @return string + */ + public function getUserNameAttribute() + { + return $this->userNameAttribute; + } + + /** + * Set the attribute name where to find a group's name + * + * @param string $groupNameAttribute + * + * @return $this + */ + public function setGroupNameAttribute($groupNameAttribute) + { + $this->groupNameAttribute = $this->getNormedAttribute($groupNameAttribute); + return $this; + } + + /** + * Return the attribute name where to find a group's name + * + * @return string + */ + public function getGroupNameAttribute() + { + return $this->groupNameAttribute; + } + + /** + * Set the attribute name where to find a group's member + * + * @param string $groupMemberAttribute + * + * @return $this + */ + public function setGroupMemberAttribute($groupMemberAttribute) + { + $this->groupMemberAttribute = $this->getNormedAttribute($groupMemberAttribute); + return $this; + } + + /** + * Return the attribute name where to find a group's member + * + * @return string + */ + public function getGroupMemberAttribute() + { + return $this->groupMemberAttribute; + } + + /** + * Set the custom LDAP filter to apply on a user query + * + * @param string $filter + * + * @return $this + */ + public function setUserFilter($filter) + { + if (($filter = trim($filter))) { + $this->userFilter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on a user query + * + * @return string + */ + public function getUserFilter() + { + return $this->userFilter; + } + + /** + * Set the custom LDAP filter to apply on a group query + * + * @param string $filter + * + * @return $this + */ + public function setGroupFilter($filter) + { + if (($filter = trim($filter))) { + $this->groupFilter = $filter; + } + + return $this; + } + + /** + * Return the custom LDAP filter to apply on a group query + * + * @return string + */ + public function getGroupFilter() + { + return $this->groupFilter; + } + + /** + * Return a new query for the given columns + * + * @param array $columns The desired columns, if null all columns will be queried + * + * @return RepositoryQuery + */ + public function select(array $columns = null) + { + $query = parent::select($columns); + $query->getQuery()->setBase($this->groupBaseDn); + if ($this->groupFilter) { + // TODO(jom): This should differentiate between groups and their memberships + $query->getQuery()->where(new Expression($this->groupFilter)); + } + + return $query; + } + + /** + * Initialize this repository's query columns + * + * @return array + * + * @throws ProgrammingError In case either $this->groupNameAttribute or $this->groupClass has not been set yet + */ + protected function initializeQueryColumns() + { + if ($this->groupClass === null) { + throw new ProgrammingError('It is required to set the objectClass where to look for groups first'); + } + if ($this->groupNameAttribute === null) { + throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first'); + } + + if ($this->ds->getCapabilities()->hasAdOid()) { + $createdAtAttribute = 'whenCreated'; + $lastModifiedAttribute = 'whenChanged'; + } else { + $createdAtAttribute = 'createTimestamp'; + $lastModifiedAttribute = 'modifyTimestamp'; + } + + // TODO(jom): Fetching memberships does not work currently, we'll need some aggregate functionality! + $columns = array( + 'group' => $this->groupNameAttribute, + 'group_name' => $this->groupNameAttribute, + 'user' => $this->groupMemberAttribute, + 'user_name' => $this->groupMemberAttribute, + 'created_at' => $createdAtAttribute, + 'last_modified' => $lastModifiedAttribute + ); + return array('group' => $columns, 'group_membership' => $columns); + } + + /** + * Initialize this repository's conversion rules + * + * @return array + * + * @throws ProgrammingError In case $this->groupClass has not been set yet + */ + protected function initializeConversionRules() + { + if ($this->groupClass === null) { + throw new ProgrammingError('It is required to set the objectClass where to look for groups first'); + } + + return array( + $this->groupClass => array( + 'created_at' => 'generalized_time', + 'last_modified' => 'generalized_time' + ) + ); + } + + /** + * Validate that the requested table exists + * + * This will return $this->groupClass in case $table equals "group" or "group_membership". + * + * @param string $table The table to validate + * @param RepositoryQuery $query An optional query to pass as context + * (unused by the base implementation) + * + * @return string + * + * @throws ProgrammingError In case the given table does not exist + */ + public function requireTable($table, RepositoryQuery $query = null) + { + $table = parent::requireTable($table, $query); + if ($table === 'group' || $table === 'group_membership') { + $table = $this->groupClass; + } + + return $table; + } + + /** + * Return the groups the given user is a member of + * + * @param User $user + * + * @return array + */ + public function getMemberships(User $user) + { + $userQuery = $this->ds + ->select() + ->from($this->userClass) + ->where($this->userNameAttribute, $user->getUsername()) + ->setBase($this->userBaseDn) + ->setUsePagedResults(false); + if ($this->userFilter) { + $userQuery->where(new Expression($this->userFilter)); + } + + if (($userDn = $userQuery->fetchDn()) === null) { + return array(); + } + + $groupQuery = $this->ds + ->select() + ->from($this->groupClass, array($this->groupNameAttribute)) + ->where($this->groupMemberAttribute, $userDn) + ->setBase($this->groupBaseDn); + if ($this->groupFilter) { + $groupQuery->where(new Expression($this->groupFilter)); + } + + $groups = array(); + foreach ($groupQuery as $row) { + $groups[] = $row->{$this->groupNameAttribute}; + } + + return $groups; + } + + /** + * Apply the given configuration on this backend + * + * @param ConfigObject $config + * + * @return $this + * + * @throws ConfigurationError In case a linked user backend does not exist or is invalid + */ + public function setConfig(ConfigObject $config) + { + if ($config->backend === 'ldap') { + $defaults = $this->getOpenLdapDefaults(); + } elseif ($config->backend === 'msldap') { + $defaults = $this->getActiveDirectoryDefaults(); + } else { + $defaults = new ConfigObject(); + } + + if ($config->user_backend && $config->user_backend !== 'none') { + $userBackend = UserBackend::create($config->user_backend); + if (! $userBackend instanceof LdapUserBackend) { + throw new ConfigurationError('User backend "%s" is not of type LDAP', $config->user_backend); + } + + if ( + $this->ds->getHostname() !== $userBackend->getDataSource()->getHostname() + || $this->ds->getPort() !== $userBackend->getDataSource()->getPort() + ) { + // TODO(jom): Elaborate whether it makes sense to link directories on different hosts + throw new ConfigurationError( + 'It is required that a linked user backend refers to the ' + . 'same directory as it\'s user group backend counterpart' + ); + } + + $defaults->merge(array( + 'user_base_dn' => $userBackend->getBaseDn(), + 'user_class' => $userBackend->getUserClass(), + 'user_name_attribute' => $userBackend->getUserNameAttribute(), + 'user_filter' => $userBackend->getFilter() + )); + } + + return $this + ->setGroupBaseDn($config->base_dn) + ->setUserBaseDn($config->get('user_base_dn', $this->getGroupBaseDn())) + ->setGroupClass($config->get('group_class', $defaults->group_class)) + ->setUserClass($config->get('user_class', $defaults->user_class)) + ->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute)) + ->setUserNameAttribute($config->get('user_name_attribute', $defaults->user_name_attribute)) + ->setGroupMemberAttribute($config->get('group_member_attribute', $defaults->group_member_attribute)) + ->setGroupFilter($config->filter) + ->setUserFilter($config->user_filter); + } + + /** + * Return the configuration defaults for an OpenLDAP environment + * + * @return ConfigObject + */ + public function getOpenLdapDefaults() + { + return new ConfigObject(array( + 'group_class' => 'group', + 'user_class' => 'inetOrgPerson', + 'group_name_attribute' => 'gid', + 'user_name_attribute' => 'uid', + 'group_member_attribute' => 'member' + )); + } + + /** + * Return the configuration defaults for an ActiveDirectory environment + * + * @return ConfigObject + */ + public function getActiveDirectoryDefaults() + { + return new ConfigObject(array( + 'group_class' => 'group', + 'user_class' => 'user', + 'group_name_attribute' => 'sAMAccountName', + 'user_name_attribute' => 'sAMAccountName', + 'group_member_attribute' => 'member' + )); + } +} diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php index dd4900ea8..978860a37 100644 --- a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php +++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php @@ -21,6 +21,8 @@ class UserGroupBackend */ protected static $defaultBackends = array( 'db', + 'ldap', + 'msldap', //'ini' ); @@ -156,6 +158,11 @@ class UserGroupBackend case 'ini': $backend = new IniUserGroupBackend($resource); break; + case 'ldap': + case 'msldap': + $backend = new LdapUserGroupBackend($resource); + $backend->setConfig($backendConfig); + break; } $backend->setName($name); diff --git a/library/Icinga/Cli/Params.php b/library/Icinga/Cli/Params.php index 9e30cf19e..85d5cd26f 100644 --- a/library/Icinga/Cli/Params.php +++ b/library/Icinga/Cli/Params.php @@ -47,7 +47,12 @@ class Params $noOptionFlag = true; } elseif (!$noOptionFlag && substr($argv[$i], 0, 2) === '--') { $key = substr($argv[$i], 2); - if (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') { + $matches = array(); + if (1 === preg_match( + '/(?params[$matches[1]] = $matches[2]; + } elseif (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') { $this->params[$key] = true; } elseif (array_key_exists($key, $this->params)) { if (!is_array($this->params[$key])) { diff --git a/library/Icinga/File/Ini/IniEditor.php b/library/Icinga/File/Ini/IniEditor.php index 1f8202d9d..00bea8df4 100644 --- a/library/Icinga/File/Ini/IniEditor.php +++ b/library/Icinga/File/Ini/IniEditor.php @@ -313,14 +313,19 @@ class IniEditor */ public function getText() { - $this->cleanUpWhitespaces(); - return rtrim(implode(PHP_EOL, $this->text)) . PHP_EOL; + $this->normalizeSectionSpacing(); + + // trim leading and trailing whitespaces from generated file + $txt = trim(implode(PHP_EOL, $this->text)) . PHP_EOL; + + // replace linebreaks, unless they preceed a comment or a section + return preg_replace("/\n[\n]*([^;\[])/", "\n$1", $txt); } /** - * Remove all unneeded line breaks between sections + * normalize section spacing according to the current settings */ - private function cleanUpWhitespaces() + private function normalizeSectionSpacing() { $i = count($this->text) - 1; for (; $i > 0; $i--) { @@ -328,24 +333,18 @@ class IniEditor if ($this->isSectionDeclaration($line) && $i > 0) { $i--; $line = $this->text[$i]; - /* - * Ignore comments that are glued to the section declaration - */ + // ignore comments that are glued to the section declaration while ($i > 0 && $this->isComment($line)) { $i--; $line = $this->text[$i]; } - /* - * Remove whitespaces between the sections - */ + // remove whitespaces between the sections while ($i > 0 && preg_match('/^\s*$/', $line) === 1) { $this->deleteLine($i); $i--; $line = $this->text[$i]; } - /* - * Refresh section separators - */ + // refresh section separators if ($i !== 0 && $this->sectionSeparators > 0) { $this->insertAtLine($i + 1, str_repeat(PHP_EOL, $this->sectionSeparators - 1)); } @@ -621,6 +620,6 @@ class IniEditor private function sanitize($value) { - return str_replace('\n', '', $value); + return str_replace("\n", '', $value); } } diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php index e43c226aa..5b565a6a0 100644 --- a/library/Icinga/File/Ini/IniWriter.php +++ b/library/Icinga/File/Ini/IniWriter.php @@ -60,7 +60,7 @@ class IniWriter extends Zend_Config_Writer_FileAbstract { if (file_exists($this->_filename)) { $oldconfig = new Zend_Config_Ini($this->_filename); - $content = file_get_contents($this->_filename); + $content = trim(file_get_contents($this->_filename)); } else { $oldconfig = new Zend_Config(array()); $content = ''; diff --git a/library/Icinga/Repository/LdapRepository.php b/library/Icinga/Repository/LdapRepository.php new file mode 100644 index 000000000..7cf00ae66 --- /dev/null +++ b/library/Icinga/Repository/LdapRepository.php @@ -0,0 +1,66 @@ + + *
  • Attribute name normalization
  • + * + */ +abstract class LdapRepository extends Repository +{ + /** + * The datasource being used + * + * @var Connection + */ + protected $ds; + + /** + * Normed attribute names based on known LDAP environments + * + * @var array + */ + protected $normedAttributes = array( + 'uid' => 'uid', + 'gid' => 'gid', + 'user' => 'user', + 'group' => 'group', + 'member' => 'member', + 'inetorgperson' => 'inetOrgPerson', + 'samaccountname' => 'sAMAccountName' + ); + + /** + * Create a new LDAP repository object + * + * @param Connection $ds The data source to use + */ + public function __construct(Connection $ds) + { + parent::__construct($ds); + } + + /** + * Return the given attribute name normed to known LDAP enviroments, if possible + * + * @param string $name + * + * @return string + */ + protected function getNormedAttribute($name) + { + $loweredName = strtolower($name); + if (array_key_exists($loweredName, $this->normedAttributes)) { + return $this->normedAttributes[$loweredName]; + } + + return $name; + } +} \ No newline at end of file diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 803f4958f..98b3e0ab4 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -599,6 +599,37 @@ abstract class Repository implements Selectable return $value; } + /** + * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation + * + * @param string|null $value + * + * @return int + */ + protected function retrieveGeneralizedTime($value) + { + if ($value === null) { + return $value; + } + + if ( + ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false + || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false + ) { + return $dateTime->getTimeStamp(); + } else { + Logger::debug(sprintf( + 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".', + $value, + $this->getName() + )); + } + } + /** * Validate that the requested table exists * diff --git a/library/Icinga/Util/File.php b/library/Icinga/Util/File.php index 8dd436137..58104e2cc 100644 --- a/library/Icinga/Util/File.php +++ b/library/Icinga/Util/File.php @@ -63,7 +63,7 @@ class File extends SplFileObject throw new NotWritableError(sprintf('Path "%s" is not writable', $dirPath)); } - $file = new static($path, 'x'); + $file = new static($path, 'x+'); if (! @chmod($path, $accessMode)) { $error = error_get_last(); diff --git a/library/Icinga/Util/Translator.php b/library/Icinga/Util/Translator.php index 67825b63b..03fd865cd 100644 --- a/library/Icinga/Util/Translator.php +++ b/library/Icinga/Util/Translator.php @@ -100,7 +100,11 @@ class Translator { $contextString = "{$context}\004{$text}"; - $translation = dcgettext($domain, $contextString, LC_MESSAGES); + $translation = dcgettext( + $domain, + $contextString, + defined('LC_MESSAGES') ? LC_MESSAGES : LC_ALL + ); if ($translation == $contextString) { return $text; @@ -126,7 +130,13 @@ class Translator { $contextString = "{$context}\004{$textSingular}"; - $translation = dcngettext($domain, $contextString, $textPlural, $number, LC_MESSAGES); + $translation = dcngettext( + $domain, + $contextString, + $textPlural, + $number, + defined('LC_MESSAGES') ? LC_MESSAGES : LC_ALL + ); if ($translation == $contextString || $translation == $textPlural) { return ($number == 1 ? $textSingular : $textPlural); diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php index 48f7acdb7..08cb02388 100644 --- a/library/Icinga/Web/Form.php +++ b/library/Icinga/Web/Form.php @@ -855,8 +855,19 @@ class Form extends Zend_Form public function populate(array $defaults) { $this->create($defaults); + $this->preserveDefaults($this, $defaults); + return parent::populate($defaults); + } - foreach ($this->getElements() as $name => $_) { + /** + * Recurse the given form and unset all unchanged default values + * + * @param Zend_Form $form + * @param array $defaults + */ + protected function preserveDefaults(Zend_Form $form, array & $defaults) + { + foreach ($form->getElements() as $name => $_) { if ( array_key_exists($name, $defaults) && array_key_exists($name . static::DEFAULT_SUFFIX, $defaults) @@ -866,7 +877,9 @@ class Form extends Zend_Form } } - return parent::populate($defaults); + foreach ($form->getSubForms() as $_ => $subForm) { + $this->preserveDefaults($subForm, $defaults); + } } /** diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php index 63d92fc9f..45dd2dd9c 100644 --- a/library/Icinga/Web/Menu.php +++ b/library/Icinga/Web/Menu.php @@ -279,6 +279,11 @@ class Menu implements RecursiveIterator 'priority' => 990, 'renderer' => 'ForeignMenuItemRenderer' )); + + $this->add(t('About'), array( + 'url' => 'about', + 'priority' => 1000 + )); } } diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php index 39e8de951..06ace7f19 100644 --- a/library/Icinga/Web/View/helpers/url.php +++ b/library/Icinga/Web/View/helpers/url.php @@ -20,7 +20,12 @@ $this->addHelperFunction('url', function ($path = null, $params = null) { } else { $url = Url::fromPath($path); } + if ($params !== null) { + if ($url === $path) { + $url = clone $url; + } + $url->overwriteParams($params); } diff --git a/modules/doc/application/views/scripts/search/index.phtml b/modules/doc/application/views/scripts/search/index.phtml index 3311c47a1..c613f04df 100644 --- a/modules/doc/application/views/scripts/search/index.phtml +++ b/modules/doc/application/views/scripts/search/index.phtml @@ -1,8 +1,8 @@
    $search): ?> - isEmpty()): ?> -

    escape($title) ?>

    - - +

    escape($title) ?>

    + isEmpty() + ? $this->translate('No documentation found matching the filter') + : $search ?>
    diff --git a/modules/doc/doc/security.md b/modules/doc/doc/security.md new file mode 100644 index 000000000..023a66410 --- /dev/null +++ b/modules/doc/doc/security.md @@ -0,0 +1,65 @@ +# Security + +The monitoring module provides an additional set of restrictions and permissions +that can be used for access control. The following sections will list those +restrictions and permissions in detail: + + +## Permissions + +The Icinga Web 2 monitoring module can send commands to the current Icinga2 instance +through the command pipe. A user needs specific permissions to be able to send those +commands when using the monitoring module. + + +| Name | Permits | +|---------------------------------------------|-----------------------------------------------------------------------------| +| monitoring/command/* | Allow all commands | +| monitoring/command/schedule-check | Allow scheduling host and service checks' | +| monitoring/command/acknowledge-problem | Allow acknowledging host and service problems | +| monitoring/command/remove-acknowledgement | Allow removing problem acknowledgements | +| monitoring/command/comment/* | Allow adding and deleting host and service comments | +| monitoring/command/comment/add | Allow commenting on hosts and services | +| monitoring/command/downtime/delete | Allow deleting host and service downtimes' | +| monitoring/command/process-check-result | Allow processing host and service check results | +| monitoring/command/feature/instance | Allow processing commands for toggling features on an instance-wide basis | +| monitoring/command/feature/object | Allow processing commands for toggling features on host and service objects | +| monitoring/command/send-custom-notification | Allow sending custom notifications for hosts and services | + + +## Restrictions + +The monitoring module allows filtering objects: + + +| Keys | Restricts | +|----------------------------|-----------------------------------------------| +| monitoring/filter/objects | Applies a filter to all hosts and services | + + +This filter will affect all hosts and services. Furthermore, it will also +affect all related objects, like notifications, downtimes or events. If a +service is hidden, all notifications, downtimes on that service will be hidden too. + + +### Filter Column Names + +The following filter column names are available in filter expressions: + + +| Column | +|------------------------------------------------------| +| host | +| host_alias | +| host_display_name | +| host_name | +| hostgroup | +| hostgroup_alias | +| hostgroup_name | +| service | +| service_description | +| service_display_name | +| service_group | +| service_group_alias | +| service_group_name | +| + all custom variables prefixed with host or service | diff --git a/modules/doc/module.info b/modules/doc/module.info index 2c6f48ac9..32516de5b 100644 --- a/modules/doc/module.info +++ b/modules/doc/module.info @@ -1,4 +1,4 @@ Module: doc -Version: 2.0.0 +Version: 2.0.0-rc1 Description: Documentation module Extracts, shows and exports documentation for Icinga Web 2 and its modules. diff --git a/modules/monitoring/application/clicommands/ConferenceCommand.php b/modules/monitoring/application/clicommands/ConferenceCommand.php index c7c7f1292..d0062b1a9 100644 --- a/modules/monitoring/application/clicommands/ConferenceCommand.php +++ b/modules/monitoring/application/clicommands/ConferenceCommand.php @@ -20,7 +20,7 @@ class ConferenceCommand extends Command * Use this command in case you feel that you should be friendly. Should * be executed as follows: * - * icingacli monitoring conference welcome --watch 1 + * icingacli monitoring conference welcome --watch=1 */ public function welcomeAction() { diff --git a/modules/monitoring/application/clicommands/ListCommand.php b/modules/monitoring/application/clicommands/ListCommand.php index 9fd892652..59a98ddf2 100644 --- a/modules/monitoring/application/clicommands/ListCommand.php +++ b/modules/monitoring/application/clicommands/ListCommand.php @@ -5,7 +5,7 @@ namespace Icinga\Module\Monitoring\Clicommands; use Icinga\Module\Monitoring\Backend; use Icinga\Module\Monitoring\Cli\CliUtils; -use Icinga\Util\Format; +use Icinga\Date\DateFormatter; use Icinga\Cli\Command; use Icinga\File\Csv; use Icinga\Module\Monitoring\Plugin\PerfdataSet; @@ -124,19 +124,19 @@ class ListCommand extends Command * --verbose Show detailled output * --showsql Dump generated SQL query (DB backend only) * - * --format > + * --format=> * Dump columns in the given format. format allows $column$ - * placeholders, e.g. --format '$host$: $service$' + * placeholders, e.g. --format='$host$: $service$' * - * -- [filter] + * --[=filter] * Filter given column by optional filter. Boolean (1/0) columns are * true if no filter value is given. * * EXAMPLES * * icingacli monitoring list --unhandled - * icingacli monitoring list --host local* --service *disk* - * icingacli monitoring list --format '$host$: $service$' + * icingacli monitoring list --host=local* --service=*disk* + * icingacli monitoring list --format='$host$: $service$' */ public function statusAction() { @@ -299,7 +299,7 @@ class ListCommand extends Command $leaf, $screen->underline($row->service_description), $screen->colorize($utils->objectStateFlags('service', $row) . $perf, 'lightblue'), - ucfirst(Format::timeSince($row->service_last_state_change)) + ucfirst(DateFormatter::timeSince($row->service_last_state_change)) ); if ($this->isVerbose) { $out .= $emptyLine . preg_replace( diff --git a/modules/monitoring/application/clicommands/NrpeCommand.php b/modules/monitoring/application/clicommands/NrpeCommand.php index 190cdbe85..abb877553 100644 --- a/modules/monitoring/application/clicommands/NrpeCommand.php +++ b/modules/monitoring/application/clicommands/NrpeCommand.php @@ -27,8 +27,8 @@ class NrpeCommand extends Command * * EXAMPLE * - * icingacli monitoring nrpe 127.0.0.1 CheckMEM --ssl --MaxWarn 80% \ - * --MaxCrit 90% --type physical + * icingacli monitoring nrpe 127.0.0.1 CheckMEM --ssl --MaxWarn=80% \ + * --MaxCrit=90% --type=physical */ public function checkAction() { diff --git a/modules/monitoring/application/controllers/HostsController.php b/modules/monitoring/application/controllers/HostsController.php index 487955da4..2d1e46848 100644 --- a/modules/monitoring/application/controllers/HostsController.php +++ b/modules/monitoring/application/controllers/HostsController.php @@ -133,6 +133,8 @@ class Monitoring_HostsController extends Controller $this->view->objects = $this->hostList; $this->view->unhandledObjects = $this->hostList->getUnhandledObjects(); $this->view->problemObjects = $this->hostList->getProblemObjects(); + $this->view->acknowledgeUnhandledLink = Url::fromPath('monitoring/hosts/acknowledge-problem') + ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString()); $this->view->downtimeUnhandledLink = Url::fromPath('monitoring/hosts/schedule-downtime') ->setQueryString($this->hostList->getUnhandledObjects()->objectsFilter()->toQueryString()); $this->view->downtimeLink = Url::fromPath('monitoring/hosts/schedule-downtime') diff --git a/modules/monitoring/application/controllers/ShowController.php b/modules/monitoring/application/controllers/ShowController.php index a8a4583d1..538a75132 100644 --- a/modules/monitoring/application/controllers/ShowController.php +++ b/modules/monitoring/application/controllers/ShowController.php @@ -78,20 +78,6 @@ class Monitoring_ShowController extends Controller $this->setupPaginationControl($this->view->history, 50); } - public function servicesAction() - { - $this->setAutorefreshInterval(15); - $this->getTabs()->activate('services'); - $this->_setParam('service', ''); - // TODO: This used to be a hack and still is. Modifying query string here. - $_SERVER['QUERY_STRING'] = (string) $this->params->without('service')->set('limit', ''); - $this->view->services = $this->view->action('services', 'list', 'monitoring', array( - 'view' => 'compact', - 'sort' => 'service_description', - )); - $this->fetchHostStats(); - } - protected function fetchHostStats() { $this->view->stats = $this->backend->select()->from('statusSummary', array( @@ -228,19 +214,6 @@ class Monitoring_ShowController extends Controller ) ); } - $tabs->add( - 'services', - array( - 'title' => sprintf( - $this->translate('List all services on host %s'), - $isService ? $object->getHost()->getName() : $object->getName() - ), - 'label' => $this->translate('Services'), - 'icon' => 'services', - 'url' => 'monitoring/show/services', - 'urlParams' => $params, - ) - ); if ($this->backend->hasQuery('eventHistory')) { $tabs->add( 'history', diff --git a/modules/monitoring/application/forms/Config/Instance/RemoteInstanceForm.php b/modules/monitoring/application/forms/Config/Instance/RemoteInstanceForm.php index 8a1a34b40..244bf66d1 100644 --- a/modules/monitoring/application/forms/Config/Instance/RemoteInstanceForm.php +++ b/modules/monitoring/application/forms/Config/Instance/RemoteInstanceForm.php @@ -3,10 +3,19 @@ namespace Icinga\Module\Monitoring\Forms\Config\Instance; +use Icinga\Data\ResourceFactory; +use Icinga\Exception\ConfigurationError; use Icinga\Web\Form; class RemoteInstanceForm extends Form { + /** + * The available monitoring instance resources split by type + * + * @var array + */ + protected $resources; + /** * (non-PHPDoc) * @see Form::init() For the method documentation. @@ -16,12 +25,89 @@ class RemoteInstanceForm extends Form $this->setName('form_config_monitoring_instance_remote'); } + /** + * Load all available ssh identity resources + * + * @return $this + * + * @throws \Icinga\Exception\ConfigurationError + */ + public function loadResources() + { + $resourceConfig = ResourceFactory::getResourceConfigs(); + + $resources = array(); + foreach ($resourceConfig as $name => $resource) { + if ($resource->type === 'ssh') { + $resources['ssh'][$name] = $name; + } + } + + if (empty($resources)) { + throw new ConfigurationError($this->translate('Could not find any valid monitoring instance resources')); + } + + $this->resources = $resources; + + return $this; + } + /** * (non-PHPDoc) * @see Form::createElements() For the method documentation. */ public function createElements(array $formData = array()) { + $useResource = isset($formData['use_resource']) ? $formData['use_resource'] : $this->getValue('use_resource'); + + $this->addElement( + 'checkbox', + 'use_resource', + array( + 'label' => $this->translate('Use SSH Identity'), + 'description' => $this->translate('Make use of the ssh identity resource'), + 'autosubmit' => true, + 'ignore' => true + ) + ); + + if ($useResource) { + + $this->loadResources(); + + $decorators = static::$defaultElementDecorators; + array_pop($decorators); // Removes the HtmlTag decorator + + $this->addElement( + 'select', + 'resource', + array( + 'required' => true, + 'label' => $this->translate('SSH Identity'), + 'description' => $this->translate('The resource to use'), + 'decorators' => $decorators, + 'multiOptions' => $this->resources['ssh'], + 'value' => current($this->resources['ssh']), + 'autosubmit' => false + ) + ); + $resourceName = isset($formData['resource']) ? $formData['resource'] : $this->getValue('resource'); + $this->addElement( + 'note', + 'resource_note', + array( + 'escape' => false, + 'decorators' => $decorators, + 'value' => sprintf( + '%3$s', + $this->getView()->url('config/editresource', array('resource' => $resourceName)), + sprintf($this->translate('Show the configuration of the %s resource'), $resourceName), + $this->translate('Show resource configuration') + ) + ) + ); + } + $this->addElements(array( array( 'text', @@ -43,8 +129,11 @@ class RemoteInstanceForm extends Form 'description' => $this->translate('SSH port to connect to on the remote Icinga instance'), 'value' => 22 ) - ), - array( + ) + )); + + if (! $useResource) { + $this->addElement( 'text', 'user', array( @@ -55,18 +144,20 @@ class RemoteInstanceForm extends Form . ' possible for this user' ) ) - ), + ); + } + + $this->addElement( + 'text', + 'path', array( - 'text', - 'path', - array( - 'required' => true, - 'label' => $this->translate('Command File'), - 'value' => '/var/run/icinga2/cmd/icinga2.cmd', - 'description' => $this->translate('Path to the Icinga command file on the remote Icinga instance') - ) + 'required' => true, + 'label' => $this->translate('Command File'), + 'value' => '/var/run/icinga2/cmd/icinga2.cmd', + 'description' => $this->translate('Path to the Icinga command file on the remote Icinga instance') ) - )); + ); + return $this; } } diff --git a/modules/monitoring/application/forms/Config/InstanceConfigForm.php b/modules/monitoring/application/forms/Config/InstanceConfigForm.php index b2baea89c..c44e480ac 100644 --- a/modules/monitoring/application/forms/Config/InstanceConfigForm.php +++ b/modules/monitoring/application/forms/Config/InstanceConfigForm.php @@ -143,6 +143,11 @@ class InstanceConfigForm extends ConfigForm $instanceConfig = $this->config->getSection($instanceName)->toArray(); $instanceConfig['name'] = $instanceName; + + if (isset($instanceConfig['resource'])) { + $instanceConfig['use_resource'] = true; + } + $this->populate($instanceConfig); } } diff --git a/modules/monitoring/application/views/scripts/list/hosts.phtml b/modules/monitoring/application/views/scripts/list/hosts.phtml index 2a0bdb8ab..fd6fd1a23 100644 --- a/modules/monitoring/application/views/scripts/list/hosts.phtml +++ b/modules/monitoring/application/views/scripts/list/hosts.phtml @@ -69,7 +69,7 @@ if (count($hosts) === 0) { $this->translatePlural('%u unhandled service', '%u unhandled services', $host->host_unhandled_services), $host->host_unhandled_services ), - 'monitoring/show/services', + 'monitoring/list/services', array( 'host' => $host->host_name, 'service_problem' => 1, diff --git a/modules/monitoring/application/views/scripts/list/servicegrid.phtml b/modules/monitoring/application/views/scripts/list/servicegrid.phtml index a9cbac445..73fbfe3b5 100644 --- a/modules/monitoring/application/views/scripts/list/servicegrid.phtml +++ b/modules/monitoring/application/views/scripts/list/servicegrid.phtml @@ -67,7 +67,7 @@ foreach ($serviceDescriptions as $service_description): ?> qlink( $host_name, - 'monitoring/show/services?' . $serviceFilter, + 'monitoring/list/services?' . $serviceFilter, array('host' => $host_name), array('title' => sprintf($this->translate('List all reported services on host %s'), $host_name)) ); ?> diff --git a/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml b/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml index 04408808f..b69682e1b 100644 --- a/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml +++ b/modules/monitoring/application/views/scripts/partials/host/servicesummary.phtml @@ -12,8 +12,7 @@ function urlAddFilterOptional($url, $filter, $optional) { return $url->setQueryString($f->toQueryString()); } -$selfUrl = Url::fromPath('monitoring/show/services', array('host' => $object->host_name)); -$currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl(); +$selfUrl = Url::fromPath('monitoring/list/services', array('host' => $object->host_name)); ?>
    compact ? ' data-base-target="col1"' : ''; ?>> stats->services_total): ?> qlink( @@ -27,15 +26,18 @@ $currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl(); ), $selfUrl, null, - array('title' => sprintf( - $this->translatePlural( - 'List all %u service on host %s', - 'List all %u services on host %s', - $object->stats->services_total + array( + 'title' => sprintf( + $this->translatePlural( + 'List all %u service on host %s', + 'List all %u services on host %s', + $object->stats->services_total + ), + $object->stats->services_total, + $object->host_name ), - $object->stats->services_total, - $object->host_name - )) + 'data-base-target' => '_next' + ) ); ?> translate('No services configured on this host'); ?> @@ -43,20 +45,23 @@ $currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl(); stats->services_ok): ?> - + qlink( $object->stats->services_ok, $selfUrl, array('service_state' => 0), - array('title' => sprintf( - $this->translatePlural( - 'List %u service that is currently in state OK on host %s', - 'List %u services which are currently in state OK on host %s', - $object->stats->services_ok + array( + 'title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state OK on host %s', + 'List %u services which are currently in state OK on host %s', + $object->stats->services_ok + ), + $object->stats->services_ok, + $object->host_name ), - $object->stats->services_ok, - $object->host_name - )) + 'data-base-target' => '_next' + ) ); ?> @@ -68,61 +73,49 @@ foreach (array(2 => 'critical', 3 => 'unknown', 1 => 'warning') as $stateId => $ $unhandled = $pre . '_unhandled'; $paramsHandled = array('service_state' => $stateId, 'service_handled' => 1); $paramsUnhandled = array('service_state' => $stateId, 'service_handled' => 0); - if ($object->stats->$unhandled) { - $compareUrl = $selfUrl->with($paramsUnhandled)->getRelativeUrl(); - } else { - $compareUrl = $selfUrl->with($paramsHandled)->getRelativeUrl(); - } - - if ($compareUrl === $currentUrl) { - $active = ' active'; - } else { - $active = ''; - } - - echo ''; + echo ''; if ($object->stats->$unhandled) { echo $this->qlink( $object->stats->$unhandled, $selfUrl, $paramsUnhandled, - array('title' => sprintf( - $this->translatePlural( - 'List %u service that is currently in state %s on host %s', - 'List %u services which are currently in state %s on host %s', - $object->stats->$unhandled + array( + 'title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state %s on host %s', + 'List %u services which are currently in state %s on host %s', + $object->stats->$unhandled + ), + $object->stats->$unhandled, + Service::getStateText($stateId, true), + $object->host_name ), - $object->stats->$unhandled, - Service::getStateText($stateId, true), - $object->host_name - )) + 'data-base-target' => '_next' + ) ); } if ($object->stats->$handled) { - - if ($selfUrl->with($paramsHandled)->getRelativeUrl() === $currentUrl) { - $active = ' active'; - } else { - $active = ''; - } if ($object->stats->$unhandled) { - echo ''; + echo ''; } echo $this->qlink( $object->stats->$handled, $selfUrl, $paramsHandled, - array('title' => sprintf( - $this->translatePlural( - 'List %u service that is currently in state %s (Acknowledged) on host %s', - 'List %u services which are currently in state %s (Acknowledged) on host %s', - $object->stats->$handled + array( + 'title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state %s (Acknowledged) on host %s', + 'List %u services which are currently in state %s (Acknowledged) on host %s', + $object->stats->$handled + ), + $object->stats->$handled, + Service::getStateText($stateId, true), + $object->host_name ), - $object->stats->$handled, - Service::getStateText($stateId, true), - $object->host_name - )) + 'data-base-target' => '_next' + ) ); if ($object->stats->$unhandled) { echo "\n"; @@ -133,22 +126,25 @@ foreach (array(2 => 'critical', 3 => 'unknown', 1 => 'warning') as $stateId => $ } ?> stats->services_pending): ?> - + qlink( $object->stats->services_pending, $selfUrl, array('service_state' => 99), - array('title' => sprintf( - $this->translatePlural( - 'List %u service that is currently in state PENDING on host %s', - 'List %u services which are currently in state PENDING on host %s', - $object->stats->services_pending + array( + 'title' => sprintf( + $this->translatePlural( + 'List %u service that is currently in state PENDING on host %s', + 'List %u services which are currently in state PENDING on host %s', + $object->stats->services_pending + ), + $object->stats->services_pending, + $object->host_name ), - $object->stats->services_pending, - $object->host_name - )) + 'data-base-target' => '_next' + ) ) ?> -
    \ No newline at end of file +
    diff --git a/modules/monitoring/application/views/scripts/show/components/notifications.phtml b/modules/monitoring/application/views/scripts/show/components/notifications.phtml index 5fea60cc1..9a9195862 100644 --- a/modules/monitoring/application/views/scripts/show/components/notifications.phtml +++ b/modules/monitoring/application/views/scripts/show/components/notifications.phtml @@ -9,7 +9,7 @@ echo $this->qlink( $this->translate('Send notification'), 'monitoring/host/send-custom-notification', - array('host_name' => $object->getName()), + array('host' => $object->getName()), array( 'icon' => 'bell', 'data-base-target' => '_self', @@ -23,7 +23,7 @@ echo $this->qlink( $this->translate('Send notification'), 'monitoring/service/send-custom-notification', - array('host_name' => $object->getHost()->getName(), 'service_description' => $object->getName()), + array('host' => $object->getHost()->getName(), 'service' => $object->getName()), array( 'icon' => 'bell', 'data-base-target' => '_self', diff --git a/modules/monitoring/application/views/scripts/show/services.phtml b/modules/monitoring/application/views/scripts/show/services.phtml deleted file mode 100644 index d3a0c3ef8..000000000 --- a/modules/monitoring/application/views/scripts/show/services.phtml +++ /dev/null @@ -1,8 +0,0 @@ -
    - compact): ?> - tabs; ?> - -render('partials/host/object-header.phtml') ?> -render('partials/host/servicesummary.phtml') ?> -
    - diff --git a/modules/monitoring/application/views/scripts/timeline/index.phtml b/modules/monitoring/application/views/scripts/timeline/index.phtml index d0cc8d34c..209eb0f1b 100644 --- a/modules/monitoring/application/views/scripts/timeline/index.phtml +++ b/modules/monitoring/application/views/scripts/timeline/index.phtml @@ -132,8 +132,8 @@ $extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$g