Merge remote-tracking branch 'origin/master' into feature/refine-ui-for-rc1-9361

This commit is contained in:
Thomas Gelf 2015-06-15 23:08:17 +02:00
commit 0170d9941a
72 changed files with 2436 additions and 402 deletions

3
.gitattributes vendored
View File

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

View File

@ -18,6 +18,7 @@ Marius Hein <marius.hein@netways.de>
Markus Frosch <markus@lazyfrosch.de>
Matthias Jentsch <matthias.jentsch@netways.de>
Michael Friedrich <michael.friedrich@netways.de>
Paul Richards <paul@minimoo.org>
Rene Moser <rene.moser@swisstxt.ch>
Susanne Vestner-Ludwig <susanne.vestner-ludwig@inserteffect.com>
Sylph Lin <sylph.lin@gmail.com>

1
application/VERSION Normal file
View File

@ -0,0 +1 @@
$Format:%H%d %ci$

View File

@ -0,0 +1,15 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
# namespace Icinga\Application\Controllers;
use Icinga\Web\Controller\ActionController;
use Icinga\Application\Version;
class AboutController extends ActionController
{
public function indexAction()
{
$this->view->version = Version::get();
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,147 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\Resource;
use Icinga\Application\Icinga;
use Icinga\Data\ConfigObject;
use Icinga\Forms\Config\ResourceConfigForm;
use Icinga\Web\Form;
use Icinga\Util\File;
use Zend_Validate_Callback;
/**
* Form class for adding/modifying ssh identity resources
*/
class SshResourceForm extends Form
{
/**
* Initialize this form
*/
public function init()
{
$this->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(
'<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
$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;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,310 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Icinga\Application\Config;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Protocol\Ldap\Connection;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form for managing LDAP user group backends
*/
class LdapUserGroupBackendForm extends Form
{
/**
* Initialize this form
*/
public function init()
{
$this->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;
}
}

View File

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

View File

@ -0,0 +1,25 @@
<div class="content">
<h1>Icinga Web 2</h1>
<?php
$versionInfo = array();
if ($version !== false) {
foreach (array(
'appVersion' => $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)
? '<p class="message-error">' . $this->translate(
'Can\'t determine Icinga Web 2\'s version'
)
: '<p>' . nl2br(implode("\n", $versionInfo), false)
) . '</p>';
?>
</div>

View File

@ -18,7 +18,7 @@ if (! $this->compact): ?>
<div class="content groups">
<?php
if ($backend === null) {
if (! isset($backend)) {
echo $this->translate('No backend found which is able to list groups') . '</div>';
return;
} else {

View File

@ -31,15 +31,16 @@ if ($this->hasPermission('config/authentication/groups/edit') && $backend instan
<p class="group-name"><strong><?= $this->escape($group->group_name); ?></strong></p> <?= $editLink; ?>
<p class="group-created"><strong><?= $this->translate('Created at'); ?>:</strong> <?= $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?></p>
<p class="group-modified"><strong><?= $this->translate('Last modified'); ?>:</strong> <?= $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?></p>
<h3><?= $this->translate('Members'); ?></h3>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content members" data-base-target="_next">
<?php if (count($members) > 0): ?>

View File

@ -67,7 +67,7 @@
</tbody>
</table>
<?php endif ?>
<a data-base-target="_next" href="<?= $this->href('role/new') ?>">
<a data-base-target="_next" href="<?= $this->href('role/add') ?>">
<?= $this->translate('Create a New Role') ?>
</a>
</div>

View File

@ -18,7 +18,7 @@ if (! $this->compact): ?>
<div class="content users">
<?php
if ($backend === null) {
if (! isset($backend)) {
echo $this->translate('No backend found which is able to list users') . '</div>';
return;
} else {

View File

@ -31,15 +31,16 @@ if ($this->hasPermission('config/authentication/users/edit') && $backend instanc
<p class="user-state"><strong><?= $this->translate('State'); ?>:</strong> <?= $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?></p>
<p class="user-created"><strong><?= $this->translate('Created at'); ?>:</strong> <?= $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?></p>
<p class="user-modified"><strong><?= $this->translate('Last modified'); ?>:</strong> <?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></p>
<h3><?= $this->translate('Group Memberships'); ?></h3>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content memberships" data-base-target="_next">
<?php if (count($memberships) > 0): ?>

70
doc/about.md Normal file
View File

@ -0,0 +1,70 @@
# <a id="about"></a> 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.
## <a id="about-monitoring"></a> 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.
## <a id="about-installation"></a> 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.
## <a id="about-configuration"></a> Configuration
Icinga Web 2 can be configured via the user interface and .ini files.
See [here](configuration#configuration) for more information about the configuration.
## <a id="about-authentication"></a> 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.
## <a id="about-authorization"></a> 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.
## <a id="about-preferences"></a> 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.
## <a id="about-documentation"></a> 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.
## <a id="about-translation"></a> 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

View File

@ -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.
### <a id="resources-configuration-database"></a> Database
@ -64,6 +64,26 @@ bind_dn = "cn=admin,ou=people,dc=icinga,dc=org"
bind_pw = admin`
````
### <a id="resources-configuration-ssh"></a> 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"
````
### <a id="resources-configuration-livestatus"></a> Livestatus
A Livestatus resource represents the location of a Livestatus socket which is used for fetching monitoring data.

279
doc/security.md Normal file
View File

@ -0,0 +1,279 @@
# <a id="security"></a> 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
<a href="http://docs.icinga.org/icinga2/latest/doc/module/icinga2/toc#!/icinga2/latest/doc/module/icinga2/chapter/getting-started#setting-up-external-command-pipe">Command Pipe</a>
in the monitoring module. All actions must be be **allowed explicitly** using permissions.
A permission is a simple list of identifiers of actions a user is
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).
<div class="info-box">
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.
</div>
#### 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.
#### <a id="syntax"></a> 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
## <a id="permissions"></a> 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).
## <a id="restrictions"></a> Restrictions
Restrictions can be used to define what a user or group can see by specifying
a filter expression that applies to a defined set of data. By default, when no
restrictions are defined, a user will be able to see every information that is available.
A 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
#### <a id="stacking-filters"></a> 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.

View File

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

View File

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

View File

@ -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('/(?<!.)[ \t]*#/ms', $osInfo))) {
return false;
}
if ($res === 1) {
continue;
}
$matches = array();
if (false === ($res = @preg_match(
'/(?<!.)[ \t]*ID[ \t]*=[ \t]*(\'|"|)(.*?)(?:\1)[ \t]*(?!.)/msi',
$osInfo,
$matches
))) {
return false;
}
if (! ($res === 0 || $matches[2] === '' || $matches[2] === 'linux')) {
return $matches[2];
}
}
}
if ($reliable > 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
*

View File

@ -0,0 +1,37 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Application;
class Version
{
/**
* Get the version of this instance of Icinga Web 2
*
* @return array|bool array on success, false otherwise
*/
public static function get()
{
if (false === ($appVersion = @file_get_contents(
Icinga::app()->getApplicationDir() . DIRECTORY_SEPARATOR . 'VERSION'
))) {
return false;
}
$matches = array();
if (false === ($res = preg_match(
'/(?<!.)\s*(?P<gitCommitID>\w+)(?:\s*\(.*?(?:(?<=[\(,])\s*tag\s*:\s*v(?P<appVersion>.+?)\s*(?=[\),]).*?)?\))?\s*(?P<gitCommitDate>\S+)/ms',
$appVersion,
$matches
)) || $res === 0) {
return false;
}
foreach ($matches as $key => $value) {
if (is_int($key) || $value === '') {
unset($matches[$key]);
}
}
return $matches;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,627 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\LdapUserBackend;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\ProgrammingError;
use Icinga\Protocol\Ldap\Expression;
use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\User;
class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBackendInterface
{
/**
* The base DN to use for a user query
*
* @var string
*/
protected $userBaseDn;
/**
* The base DN to use for a group query
*
* @var string
*/
protected $groupBaseDn;
/**
* The objectClass where look for users
*
* @var string
*/
protected $userClass;
/**
* The objectClass where look for groups
*
* @var string
*/
protected $groupClass;
/**
* The attribute name where to find a user's name
*
* @var string
*/
protected $userNameAttribute;
/**
* The attribute name where to find a group's name
*
* @var string
*/
protected $groupNameAttribute;
/**
* The attribute name where to find a group's member
*
* @var string
*/
protected $groupMemberAttribute;
/**
* The custom LDAP filter to apply on a user query
*
* @var string
*/
protected $userFilter;
/**
* The custom LDAP filter to apply on a group query
*
* @var string
*/
protected $groupFilter;
/**
* The columns which are not permitted to be queried
*
* @var array
*/
protected $filterColumns = array('group', 'user');
/**
* The default sort rules to be applied on a query
*
* @var array
*/
protected $sortRules = array(
'group_name' => 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'
));
}
}

View File

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

View File

@ -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(
'/(?<!.)([^=]+)=(.*)(?!.)/ms', $key, $matches
)) {
$this->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])) {

View File

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

View File

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

View File

@ -0,0 +1,66 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Icinga\Protocol\Ldap\Connection;
/**
* Abstract base class for concrete LDAP repository implementations
*
* Additionally provided features:
* <ul>
* <li>Attribute name normalization</li>
* </ul>
*/
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -279,6 +279,11 @@ class Menu implements RecursiveIterator
'priority' => 990,
'renderer' => 'ForeignMenuItemRenderer'
));
$this->add(t('About'), array(
'url' => 'about',
'priority' => 1000
));
}
}

View File

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

View File

@ -1,8 +1,8 @@
<div class="content">
<?php foreach (/** @var \Icinga\Module\Doc\Renderer\DocSearchRenderer[] $searches */ $searches as $title => $search): ?>
<?php if (! $search->isEmpty()): ?>
<h1><?= $this->escape($title) ?></h1>
<?= $search ?>
<?php endif ?>
<h2><?= $this->escape($title) ?></h2>
<?= $search->isEmpty()
? $this->translate('No documentation found matching the filter')
: $search ?>
<?php endforeach ?>
</div>

View File

@ -0,0 +1,65 @@
# <a id="monitoring-security"></a> 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 |
## <a id="monitoring-security-restrictions"></a> 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 |

View File

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

View File

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

View File

@ -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 <csv|json|<custom>>
* --format=<csv|json|<custom>>
* Dump columns in the given format. <custom> format allows $column$
* placeholders, e.g. --format '$host$: $service$'
* placeholders, e.g. --format='$host$: $service$'
*
* --<column> [filter]
* --<column>[=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(

View File

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

View File

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

View File

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

View File

@ -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(
'<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
$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;
}
}

View File

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

View File

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

View File

@ -67,7 +67,7 @@ foreach ($serviceDescriptions as $service_description): ?>
<th>
<?= $this->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))
); ?>

View File

@ -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));
?><div class="tinystatesummary" <?= $this->compact ? ' data-base-target="col1"' : ''; ?>>
<?php if ($object->stats->services_total): ?>
<?= $this->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'
)
); ?>
<?php else: ?>
<?= $this->translate('No services configured on this host'); ?>
@ -43,20 +45,23 @@ $currentUrl = Url::fromRequest()->without('limit')->getRelativeUrl();
<span class="badges">
<?php if ($object->stats->services_ok): ?>
<span class="state ok<?= $currentUrl === $selfUrl->with('service_state', 0)->getRelativeUrl() ? ' active' : ''; ?>">
<span class="state ok">
<?= $this->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'
)
); ?>
</span>
<?php endif ?>
@ -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 '<span class="state ' . $state . $active . ($object->stats->$unhandled ? '' : ' handled') . '">';
echo '<span class="state ' . $state . ($object->stats->$unhandled ? '' : ' handled') . '">';
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 '<span class="state handled ' . $state . $active . '">';
echo '<span class="state handled ' . $state . '">';
}
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 "</span>\n";
@ -133,20 +126,23 @@ foreach (array(2 => 'critical', 3 => 'unknown', 1 => 'warning') as $stateId => $
}
?>
<?php if ($object->stats->services_pending): ?>
<span class="state pending<?= $currentUrl === $selfUrl->with('service_state', 99)->getRelativeUrl() ? ' active' : ''; ?>">
<span class="state pending">
<?= $this->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'
)
) ?>
</span>
<?php endif ?>

View File

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

View File

@ -1,8 +0,0 @@
<div class="controls">
<?php if (! $this->compact): ?>
<?= $this->tabs; ?>
<?php endif ?>
<?= $this->render('partials/host/object-header.phtml') ?>
<?= $this->render('partials/host/servicesummary.phtml') ?>
</div>
<?= $services ?>

View File

@ -132,8 +132,8 @@ $extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$g
<?php endforeach ?>
<a aria-hidden="true" id="end" href="<?= Url::fromRequest()->remove(
array(
'raw_timestamp<',
'raw_timestamp>'
'timestamp<',
'timestamp>'
)
)->overwriteParams(
array(

View File

@ -3,7 +3,8 @@
## Abstract
The instance.ini defines how icingaweb accesses the command pipe of your icinga process in order to submit external
commands. When you are at the root of your icingaweb installation you can find it under ./config/modules/monitoring/instances.ini.
commands. Depending on the config path (default: /etc/icingaweb2) of your icingaweb installation you can find it
under ./modules/monitoring/instances.ini.
## Syntax
@ -33,5 +34,22 @@ setup key authentication at the endpoint and allow your icingweb's user to acces
port=22 ; the port to use (22 if none is given)
user=jdoe ; the user to authenticate with
You can also make use of the ssh resource for accessing an icinga pipe with key-based authentication, which will give
you the possibility to define the location of the private key for a specific user, let's have a look:
[icinga]
path=/usr/local/icinga/var/rw/icinga.cmd ; the path on the remote machine where the icinga.cmd can be found
host=my.remote.machine.com ; the hostname or address of the remote machine
port=22 ; the port to use (22 if none is given)
resource=ssh ; the ssh resource which contains the username and the location of the private key
And the associated ssh resource:
[ssh]
type = "ssh"
user = "ssh-user"
private_key = "/etc/icingaweb2/ssh/ssh-user"

View File

@ -4,6 +4,7 @@
namespace Icinga\Module\Monitoring\Command\Transport;
use Icinga\Application\Logger;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
use Icinga\Module\Monitoring\Command\Exception\TransportException;
use Icinga\Module\Monitoring\Command\IcingaCommand;
@ -44,6 +45,13 @@ class RemoteCommandFile implements CommandTransportInterface
*/
protected $user;
/**
* Path to the private key file for the key-based authentication
*
* @var string
*/
protected $privateKey;
/**
* Path to the Icinga command file on the remote host
*
@ -137,6 +145,55 @@ class RemoteCommandFile implements CommandTransportInterface
return $this->user;
}
/**
* Set the path to the private key file
*
* @param string $privateKey
*
* @return $this
*/
public function setPrivateKey($privateKey)
{
$this->privateKey = (string) $privateKey;
return $this;
}
/**
* Get the path to the private key
*
* @return string
*/
public function getPrivateKey()
{
return $this->privateKey;
}
/**
* Use a given resource to set the user and the key
*
* @param string
*
* @throws ConfigurationError
*/
public function setResource($resource = null)
{
$config = ResourceFactory::getResourceConfig($resource);
if (! isset($config->user)) {
throw new ConfigurationError(
t("Can't send external Icinga Command. Remote user is missing")
);
}
if (! isset($config->private_key)) {
throw new ConfigurationError(
t("Can't send external Icinga Command. The private key for the remote user is missing")
);
}
$this->setUser($config->user);
$this->setPrivateKey($config->private_key);
}
/**
* Set the path to the Icinga command file on the remote host
*
@ -192,6 +249,9 @@ class RemoteCommandFile implements CommandTransportInterface
if (isset($this->user)) {
$ssh .= sprintf(' -l %s', escapeshellarg($this->user));
}
if (isset($this->privateKey)) {
$ssh .= sprintf(' -o StrictHostKeyChecking=no -i %s', escapeshellarg($this->privateKey));
}
$ssh .= sprintf(
' %s "echo %s > %s" 2>&1', // Redirect stderr to stdout
escapeshellarg($this->host),

View File

@ -181,19 +181,6 @@ abstract class MonitoredObjectController 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',

View File

@ -1,5 +1,5 @@
Module: monitoring
Version: 2.0.0~alpha4
Version: 2.0.0-rc1
Description: Icinga monitoring module
This is the core module for most Icingaweb users. It provides an
abstraction layer for various Icinga data backends.

View File

@ -29,7 +29,7 @@ class ConfigCommand extends Command
*
* icingacli setup config directory
*
* icingacli setup config directory --mode 2775 --config /opt/icingaweb2/etc
* icingacli setup config directory --mode=2775 --config=/opt/icingaweb2/etc
*/
public function directoryAction()
{
@ -96,7 +96,7 @@ class ConfigCommand extends Command
*
* --path=<urlpath> The URL path to Icinga Web 2 [/icingaweb2]
*
* --root/--document-root=<directory> The directory from which the webserver will serve files [/path/to/icingaweb2/public]
* --root|--document-root=<directory> The directory from which the webserver will serve files [/path/to/icingaweb2/public]
*
* --config=<directory> Path to Icinga Web 2's configuration files [/etc/icingaweb2]
*
@ -106,9 +106,9 @@ class ConfigCommand extends Command
*
* icingacli setup config webserver apache
*
* icingacli setup config webserver apache --path /icingaweb2 --document-root /usr/share/icingaweb2/public --config=/etc/icingaweb2
* icingacli setup config webserver apache --path=/icingaweb2 --document-root=/usr/share/icingaweb2/public --config=/etc/icingaweb2
*
* icingacli setup config webserver apache --file /etc/apache2/conf.d/icingaweb2.conf
* icingacli setup config webserver apache --file=/etc/apache2/conf.d/icingaweb2.conf
*
* icingacli setup config webserver nginx
*/

View File

@ -10,6 +10,39 @@ $configDir = Icinga::app()->getConfigDir();
$setupTokenPath = rtrim($configDir, '/') . '/setup.token';
$cliPath = realpath(Icinga::app()->getApplicationDir() . '/../bin/icingacli');
$groupadd = null;
$usermod = null;
if (! (false === ($distro = Platform::getLinuxDistro(1)) || $distro === 'linux')) {
foreach (array(
'groupadd -r icingaweb2' => array(
'redhat', 'rhel', 'centos', 'fedora',
'suse', 'sles', 'sled', 'opensuse'
),
'addgroup --system icingaweb2' => array('debian', 'ubuntu')
) as $groupadd_ => $distros) {
if (in_array($distro, $distros)) {
$groupadd = $groupadd_;
break;
}
}
foreach (array(
'usermod -a -G icingaweb2 apache' => array(
'redhat', 'rhel', 'centos', 'fedora'
),
'usermod -A icingaweb2 wwwrun' => array(
'suse', 'sles', 'sled', 'opensuse'
),
'usermod -a -G icingaweb2 www-data' => array(
'debian', 'ubuntu'
)
) as $usermod_ => $distros) {
if (in_array($distro, $distros)) {
$usermod = $usermod_;
break;
}
}
}
?>
<div class="welcome-page">
<h2><?= $this->translate('Welcome to the configuration of Icinga Web 2!') ?></h2>
@ -51,6 +84,12 @@ $cliPath = realpath(Icinga::app()->getApplicationDir() . '/../bin/icingacli');
<li><?= $this->translate('Your webserver\'s user is a member of the system group "icingaweb2"'); ?></li>
<?php endif ?>
</ul>
<?php if (! ($groupadd === null || $usermod === null)) { ?>
<div class="code">
<span><?= $this->escape($groupadd . ';') ?></span>
<span><?= $this->escape($usermod . ';') ?></span>
</div>
<?php } ?>
<p><?= $this->translate('If you\'ve got the IcingaCLI installed you can do the following:'); ?></p>
<div class="code">
<span><?= $cliPath ? $cliPath : 'icingacli'; ?> setup config directory --group icingaweb2<?= $configDir !== '/etc/icingaweb2' ? ' --config ' . $configDir : ''; ?>;</span>

View File

@ -0,0 +1,6 @@
Module: setup
Version: 2.0.0-rc1
Description: Setup module
Web based wizard for setting up Icinga Web 2 and its modules.
This includes the data backends (e.g. relational database, LDAP),
the authentication method, where to store the user preferences and much more.

View File

@ -41,7 +41,7 @@ class PhpCommand extends Command
*
* icingacli test php unit --verbose
* icingacli test php unit --build
* icingacli test php unit --include *SpecialTest
* icingacli test php unit --include=*SpecialTest
*/
public function unitAction()
{
@ -109,8 +109,8 @@ class PhpCommand extends Command
*
* icingacli test php style --verbose
* icingacli test php style --build
* icingacli test php style --include path/to/your/file
* icingacli test php style --exclude *someFile* --exclude someOtherFile*
* icingacli test php style --include=path/to/your/file
* icingacli test php style --exclude=*someFile* --exclude=someOtherFile*
*/
public function styleAction()
{

View File

@ -1,5 +1,5 @@
Module: test
Version: 2.0.0~alpha4
Version: 2.0.0-rc1
Description: Translation module
This module allows developers to run (unit) tests against Icinga Web 2 and
any of its modules. Usually you do not need to enable this.

View File

@ -1,7 +1,7 @@
Module: translation
Version: 2.0.0~alpha4
Version: 2.0.0-rc1
Description: Translation module
This module allows developers and translators to translate Icinga Web 2 and
its modules for multiple languages. You do not need this module to run an
internationalized web frontend. This is only for people who want to contribute
translations or translate just their own moduls.
translations or translate just their own modules.

View File

@ -210,6 +210,13 @@ textarea {
height: 4em;
}
textarea.resource {
&.ssh-identity {
width: 50%;
height: 25em;
}
}
form .description {
font-size: 0.8em;
margin: 0.3em 0 0 0.6em;

View File

@ -336,8 +336,7 @@ html {
position: absolute;
}
/* TODO: replace this with .error */
.fileNotReadable {
.message-error {
padding: 0.5em;
background-color: @colorCritical;
font-weight: bold;

View File

@ -304,9 +304,6 @@ div.content.users {
}
div.controls div.user-header {
border-bottom: 2px solid @colorPetrol;
margin-bottom: 1em;
.user-name {
display: inline-block;
margin: 0 0 0.3em;
@ -370,9 +367,6 @@ div.content.groups {
}
div.controls div.group-header {
border-bottom: 2px solid @colorPetrol;
margin-bottom: 1em;
.group-name {
display: inline-block;
margin: 0 0 0.3em;

View File

@ -27,10 +27,9 @@ class LdapBackendFormTest extends BaseTestCase
*/
public function testValidBackendIsValid()
{
$this->setUpResourceFactoryMock();
Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
->shouldReceive('assertAuthenticationPossible')->andReturnNull()
->shouldReceive('setConfig')->andReturnNull();
$ldapUserBackendMock = Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend');
$ldapUserBackendMock->shouldReceive('assertAuthenticationPossible')->andReturnNull();
$this->setUpUserBackendMock($ldapUserBackendMock);
// Passing array(null) is required to make Mockery call the constructor...
$form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
@ -53,9 +52,9 @@ class LdapBackendFormTest extends BaseTestCase
*/
public function testInvalidBackendIsNotValid()
{
$this->setUpResourceFactoryMock();
Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException);
$ldapUserBackendMock = Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend');
$ldapUserBackendMock->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException);
$this->setUpUserBackendMock($ldapUserBackendMock);
// Passing array(null) is required to make Mockery call the constructor...
$form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
@ -72,12 +71,10 @@ class LdapBackendFormTest extends BaseTestCase
);
}
protected function setUpResourceFactoryMock()
protected function setUpUserBackendMock($ldapUserBackendMock)
{
Mockery::mock('alias:Icinga\Data\ResourceFactory')
->shouldReceive('createResource')
->andReturn(Mockery::mock('Icinga\Protocol\Ldap\Connection'))
->shouldReceive('getResourceConfig')
->andReturn(new ConfigObject());
Mockery::mock('alias:Icinga\Authentication\User\UserBackend')
->shouldReceive('create')
->andReturn($ldapUserBackendMock);
}
}

View File

@ -719,6 +719,34 @@ EOD;
);
}
public function testWhetherLinebreaksAreRemoved()
{
$target = $this->writeConfigToTemporaryFile('');
$writer = new IniWriter(
array(
'config' => Config::fromArray(
array(
'section' => array(
'foo' => 'linebreak
in line',
'linebreak
inkey' => 'blarg'
)
)
),
'filename' => $target
)
);
$rendered = $writer->render();
var_dump($rendered);
$this->assertEquals(
count(explode("\n", $rendered)),
4,
'generated config should not contain more than three line breaks'
);
}
/**
* Write a INI-configuration string to a temporary file and return its path
*