diff --git a/.gitattributes b/.gitattributes
index a05c2f5cd..6ae8ee7af 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -6,3 +6,6 @@ Vagrantfile export-ignore
# Normalize puppet manifests' line endings to LF on checkin and prevent conversion to CRLF when the files are checked out
.puppet* eol=lf
+
+# Include version information on `git archive'
+/application/VERSION export-subst
diff --git a/application/VERSION b/application/VERSION
new file mode 100644
index 000000000..519b667a7
--- /dev/null
+++ b/application/VERSION
@@ -0,0 +1 @@
+$Format:%H%d %ci$
diff --git a/application/controllers/AboutController.php b/application/controllers/AboutController.php
new file mode 100644
index 000000000..a4247f234
--- /dev/null
+++ b/application/controllers/AboutController.php
@@ -0,0 +1,15 @@
+view->version = Version::get();
+ }
+}
diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
index 8f2c33e62..a4161981a 100644
--- a/application/controllers/GroupController.php
+++ b/application/controllers/GroupController.php
@@ -28,6 +28,10 @@ class GroupController extends AuthBackendController
function ($b) { return $b->getName(); },
$this->loadUserGroupBackends('Icinga\Data\Selectable')
);
+ if (empty($backendNames)) {
+ return;
+ }
+
$this->view->backendSelection = new Form();
$this->view->backendSelection->setAttrib('class', 'backend-selection');
$this->view->backendSelection->setUidDisabled();
@@ -100,6 +104,7 @@ class GroupController extends AuthBackendController
$filterEditor = Widget::create('filterEditor')
->setQuery($members)
+ ->setSearchColumns(array('user'))
->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'group')
->ignoreParams('page')
->handleRequest($this->getRequest());
diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
index e944af52c..54482dd7d 100644
--- a/application/controllers/UserController.php
+++ b/application/controllers/UserController.php
@@ -28,6 +28,10 @@ class UserController extends AuthBackendController
function ($b) { return $b->getName(); },
$this->loadUserBackends('Icinga\Data\Selectable')
);
+ if (empty($backendNames)) {
+ return;
+ }
+
$this->view->backendSelection = new Form();
$this->view->backendSelection->setAttrib('class', 'backend-selection');
$this->view->backendSelection->setUidDisabled();
@@ -99,6 +103,7 @@ class UserController extends AuthBackendController
$filterEditor = Widget::create('filterEditor')
->setQuery($memberships)
+ ->setSearchColumns(array('group_name'))
->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'user')
->ignoreParams('page')
->handleRequest($this->getRequest());
diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php
index cdb6826be..1a3c2f46e 100644
--- a/application/controllers/UsergroupbackendController.php
+++ b/application/controllers/UsergroupbackendController.php
@@ -52,7 +52,7 @@ class UsergroupbackendController extends Controller
$form->setIniConfig(Config::app('groups'));
$form->setOnSuccess(function (UserGroupBackendForm $form) {
try {
- $form->add($form->getValues());
+ $form->add(array_filter($form->getValues()));
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
@@ -85,7 +85,12 @@ class UsergroupbackendController extends Controller
$form->setIniConfig(Config::app('groups'));
$form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) {
try {
- $form->edit($backendName, $form->getValues());
+ $form->edit($backendName, array_map(
+ function ($v) {
+ return $v !== '' ? $v : null;
+ },
+ $form->getValues()
+ ));
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php
index c520ec68f..ac2091398 100644
--- a/application/forms/Config/UserBackend/LdapBackendForm.php
+++ b/application/forms/Config/UserBackend/LdapBackendForm.php
@@ -8,7 +8,7 @@ use Icinga\Web\Form;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\AuthenticationException;
-use Icinga\Authentication\User\LdapUserBackend;
+use Icinga\Authentication\User\UserBackend;
/**
* Form class for adding/modifying LDAP user backends
@@ -48,6 +48,8 @@ class LdapBackendForm extends Form
*/
public function createElements(array $formData)
{
+ $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false;
+
$this->addElement(
'text',
'name',
@@ -77,10 +79,13 @@ class LdapBackendForm extends Form
'text',
'user_class',
array(
- 'required' => true,
- 'label' => $this->translate('LDAP User Object Class'),
- 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
- 'value' => 'inetOrgPerson'
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $isAd ? 'user' : 'inetOrgPerson'
)
);
$this->addElement(
@@ -117,12 +122,15 @@ class LdapBackendForm extends Form
'text',
'user_name_attribute',
array(
- 'required' => true,
- 'label' => $this->translate('LDAP User Name Attribute'),
- 'description' => $this->translate(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
'The attribute name used for storing the user name on the LDAP server.'
),
- 'value' => 'uid'
+ 'value' => $isAd ? 'sAMAccountName' : 'uid'
)
);
$this->addElement(
@@ -130,7 +138,7 @@ class LdapBackendForm extends Form
'backend',
array(
'disabled' => true,
- 'value' => 'ldap'
+ 'value' => $isAd ? 'msldap' : 'ldap'
)
);
$this->addElement(
@@ -170,8 +178,7 @@ class LdapBackendForm extends Form
public static function isValidUserBackend(Form $form)
{
try {
- $ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig()));
- $ldapUserBackend->setConfig(new ConfigObject($form->getValues()));
+ $ldapUserBackend = UserBackend::create(null, new ConfigObject($form->getValues()));
$ldapUserBackend->assertAuthenticationPossible();
} catch (AuthenticationException $e) {
if (($previous = $e->getPrevious()) !== null) {
@@ -193,6 +200,8 @@ class LdapBackendForm extends Form
* Return the configuration for the chosen resource
*
* @return ConfigObject
+ *
+ * @todo Check whether it's possible to drop this (Or even all occurences!)
*/
public function getResourceConfig()
{
diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php
index 0a30dd590..62a68e70b 100644
--- a/application/forms/Config/UserBackendConfigForm.php
+++ b/application/forms/Config/UserBackendConfigForm.php
@@ -60,16 +60,24 @@ class UserBackendConfigForm extends ConfigForm
*/
public function getBackendForm($type)
{
- if ($type === 'db') {
- $form = new DbBackendForm();
- $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array());
- } elseif ($type === 'ldap') {
- $form = new LdapBackendForm();
- $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array());
- } elseif ($type === 'external') {
- $form = new ExternalBackendForm();
- } else {
- throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type));
+ switch ($type)
+ {
+ case 'db':
+ $form = new DbBackendForm();
+ $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array());
+ break;
+ case 'ldap':
+ case 'msldap':
+ $form = new LdapBackendForm();
+ $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array());
+ break;
+ case 'external':
+ $form = new ExternalBackendForm();
+ break;
+ default:
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
}
return $form;
@@ -296,6 +304,7 @@ class UserBackendConfigForm extends ConfigForm
}
if (isset($this->resources['ldap']) && ($backendType === 'ldap' || Platform::extensionLoaded('ldap'))) {
$backendTypes['ldap'] = 'LDAP';
+ $backendTypes['msldap'] = 'ActiveDirectory';
}
$externalBackends = array_filter(
diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
new file mode 100644
index 000000000..c3c8baacf
--- /dev/null
+++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
@@ -0,0 +1,310 @@
+setName('form_config_ldapusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $resourceNames = $this->getLdapResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate('The LDAP connection to use for this backend.'),
+ 'multiOptions' => array_combine($resourceNames, $resourceNames)
+ )
+ );
+ $resource = ResourceFactory::create(
+ isset($formData['resource']) && in_array($formData['resource'], $resourceNames)
+ ? $formData['resource']
+ : $resourceNames[0]
+ );
+
+ $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend'));
+ $userBackendNames = $this->getLdapUserBackendNames($resource);
+ if (! empty($userBackendNames)) {
+ $userBackends = array_merge($userBackends, array_combine($userBackendNames, $userBackendNames));
+ }
+ $this->addElement(
+ 'select',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'description' => $this->translate('The user backend to link with this user group backend.'),
+ 'multiOptions' => $userBackends
+ )
+ );
+
+ $groupBackend = new LdapUserGroupBackend($resource);
+ if ($formData['type'] === 'ldap') {
+ $defaults = $groupBackend->getOpenLdapDefaults();
+ $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false!
+ } else { // $formData['type'] === 'msldap'
+ $defaults = $groupBackend->getActiveDirectoryDefaults();
+ $groupConfigDisabled = $userConfigDisabled = true;
+ }
+
+ $dnDisabled = null; // MUST BE null
+ if (isset($formData['user_backend']) && $formData['user_backend'] !== 'none') {
+ $userBackend = UserBackend::create($formData['user_backend']);
+ $defaults->merge(array(
+ 'user_base_dn' => $userBackend->getBaseDn(),
+ 'user_class' => $userBackend->getUserClass(),
+ 'user_name_attribute' => $userBackend->getUserNameAttribute(),
+ 'user_filter' => $userBackend->getFilter()
+ ));
+ $userConfigDisabled = $dnDisabled = true;
+ }
+
+ $this->createGroupConfigElements($defaults, $groupConfigDisabled);
+ $this->createUserConfigElements($defaults, $userConfigDisabled, $dnDisabled);
+ }
+
+ /**
+ * Create and add all elements to this form required for the group configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createGroupConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'group_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Object Class'),
+ 'description' => $this->translate('The object class used for storing groups on the LDAP server.'),
+ 'value' => $defaults->group_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP Group Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up groups using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->group_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a group\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->group_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP Group Base DN'),
+ 'description' => $this->translate(
+ 'The path where groups can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->base_dn
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the user configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ * @param null|bool $dnDisabled
+ */
+ protected function createUserConfigElements(ConfigObject $defaults, $disabled, $dnDisabled)
+ {
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $defaults->user_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'ignore' => $dnDisabled,
+ 'disabled' => $dnDisabled,
+ 'label' => $this->translate('LDAP User Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->user_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a user\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->user_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $dnDisabled,
+ 'disabled' => $dnDisabled,
+ 'label' => $this->translate('LDAP User Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->user_base_dn
+ )
+ );
+ }
+
+ /**
+ * Return the names of all configured LDAP resources
+ *
+ * @return array
+ */
+ protected function getLdapResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (in_array(strtolower($config->type), array('ldap', 'msldap'))) {
+ $names[] = $name;
+ }
+ }
+
+ if (empty($names)) {
+ Notification::error(
+ $this->translate('No LDAP resources available. Please configure an LDAP resource first.')
+ );
+ $this->getResponse()->redirectAndExit('config/createresource');
+ }
+
+ return $names;
+ }
+
+ /**
+ * Return the names of all configured LDAP user backends
+ *
+ * @param Connection $resource
+ *
+ * @return array
+ */
+ protected function getLdapUserBackendNames(Connection $resource)
+ {
+ $names = array();
+ foreach (Config::app('authentication') as $name => $config) {
+ if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) {
+ $backendResource = ResourceFactory::create($config->resource);
+ if (
+ $backendResource->getHostname() === $resource->getHostname()
+ && $backendResource->getPort() === $resource->getPort()
+ ) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php
index dd9875c9a..5bd0d9681 100644
--- a/application/forms/Config/UserGroup/UserGroupBackendForm.php
+++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php
@@ -13,6 +13,13 @@ use Icinga\Forms\ConfigForm;
*/
class UserGroupBackendForm extends ConfigForm
{
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
/**
* Initialize this form
*/
@@ -31,10 +38,17 @@ class UserGroupBackendForm extends ConfigForm
*/
public function getBackendForm($type)
{
- if ($type === 'db') {
- return new DbUserGroupBackendForm();
- } else {
- throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type));
+ switch ($type)
+ {
+ case 'db':
+ return new DbUserGroupBackendForm();
+ case 'ldap':
+ case 'msldap':
+ return new LdapUserGroupBackendForm();
+ default:
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
}
}
@@ -53,10 +67,7 @@ class UserGroupBackendForm extends ConfigForm
throw new NotFoundError('No user group backend called "%s" found', $name);
}
- $data = $this->config->getSection($name)->toArray();
- $data['type'] = $data['backend'];
- $data['name'] = $name;
- $this->populate($data);
+ $this->backendToLoad = $name;
return $this;
}
@@ -103,13 +114,23 @@ class UserGroupBackendForm extends ConfigForm
}
$backendConfig = $this->config->getSection($name);
- if (isset($data['name']) && $data['name'] !== $name) {
- $this->config->removeSection($name);
- $name = $data['name'];
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
unset($data['name']);
}
- $this->config->setSection($name, $backendConfig->merge($data));
+ $backendConfig->merge($data);
+ foreach ($backendConfig->toArray() as $k => $v) {
+ if ($v === null) {
+ unset($backendConfig->$k);
+ }
+ }
+
+ $this->config->setSection($name, $backendConfig);
return $this;
}
@@ -161,7 +182,9 @@ class UserGroupBackendForm extends ConfigForm
// TODO(jom): We did not think about how to configure custom group backends yet!
$backendTypes = array(
- 'db' => $this->translate('Database')
+ 'db' => $this->translate('Database'),
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
);
$backendType = isset($formData['type']) ? $formData['type'] : null;
@@ -191,8 +214,34 @@ class UserGroupBackendForm extends ConfigForm
)
);
- $backendForm = $this->getBackendForm($backendType);
- $backendForm->createElements($formData);
- $this->addElements($backendForm->getElements());
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['type'] = $data['backend'];
+ $data['name'] = $this->backendToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * @param bool $suppressArrayNotation Ignored
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues();
+ $values = array_merge($values, $values['backend_form']);
+ unset($values['backend_form']);
+ return $values;
}
}
diff --git a/application/views/scripts/about/index.phtml b/application/views/scripts/about/index.phtml
new file mode 100644
index 000000000..e270a8eb9
--- /dev/null
+++ b/application/views/scripts/about/index.phtml
@@ -0,0 +1,25 @@
+
+
Icinga Web 2
+ $this->translate('Version: %s'),
+ 'gitCommitID' => $this->translate('Git commit ID: %s'),
+ 'gitCommitDate' => $this->translate('Git commit date: %s')
+ ) as $key => $label) {
+ if (array_key_exists($key, $version) && null !== ($value = $version[$key])) {
+ $versionInfo[] = sprintf($label, htmlspecialchars($value));
+ }
+ }
+ }
+
+ echo (
+ 0 === count($versionInfo)
+ ? '
' . $this->translate(
+ 'Can\'t determine Icinga Web 2\'s version'
+ )
+ : '
' . nl2br(implode("\n", $versionInfo), false)
+ ) . '
';
+ ?>
+
diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml
index bcd9dca93..65c1591cd 100644
--- a/application/views/scripts/group/list.phtml
+++ b/application/views/scripts/group/list.phtml
@@ -18,7 +18,7 @@ if (! $this->compact): ?>
translate('No backend found which is able to list groups') . '
';
return;
} else {
diff --git a/application/views/scripts/role/list.phtml b/application/views/scripts/role/list.phtml
index 766ba26f3..568bbdf98 100644
--- a/application/views/scripts/role/list.phtml
+++ b/application/views/scripts/role/list.phtml
@@ -67,7 +67,7 @@
-
+
= $this->translate('Create a New Role') ?>
diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml
index 76a6f2b8b..df9a14a45 100644
--- a/application/views/scripts/user/list.phtml
+++ b/application/views/scripts/user/list.phtml
@@ -18,7 +18,7 @@ if (! $this->compact): ?>
translate('No backend found which is able to list users') . '
';
return;
} else {
diff --git a/icingaweb2.spec b/icingaweb2.spec
index 1ea69451d..c7aa4cb8d 100644
--- a/icingaweb2.spec
+++ b/icingaweb2.spec
@@ -218,6 +218,7 @@ rm -rf %{buildroot}
%{basedir}/application/forms
%{basedir}/application/layouts
%{basedir}/application/views
+%{basedir}/application/VERSION
%{basedir}/doc
%{basedir}/modules
%{basedir}/public
diff --git a/library/Icinga/Application/Platform.php b/library/Icinga/Application/Platform.php
index 355311b97..cc72857f9 100644
--- a/library/Icinga/Application/Platform.php
+++ b/library/Icinga/Application/Platform.php
@@ -59,6 +59,122 @@ class Platform
return strtoupper(substr(self::getOperatingSystemName(), 0, 5)) === 'LINUX';
}
+ /**
+ * Return the Linux distribution's name
+ * or 'linux' if the name could not be found out
+ * or false if the OS isn't Linux or an error occurred
+ *
+ * @param int $reliable
+ * 3: Only parse /etc/os-release (or /usr/lib/os-release).
+ * For the paranoid ones.
+ * 2: If that (3) doesn't help, check /etc/*-release, too.
+ * If something is unclear, return 'linux'.
+ * 1: Almost equal to mode 2. The possible return values also include:
+ * 'redhat' -- unclear whether RHEL/Fedora/...
+ * 'suse' -- unclear whether SLES/openSUSE/...
+ * 0: If even that (1) doesn't help, check /proc/version, too.
+ * This may not work (as expected) on LXC containers!
+ * (No reliability at all!)
+ *
+ * @return string|bool
+ */
+ public static function getLinuxDistro($reliable = 2)
+ {
+ if (! self::isLinux()) {
+ return false;
+ }
+
+ foreach (array('/etc/os-release', '/usr/lib/os-release') as $osReleaseFile) {
+ if (false === ($osRelease = @file(
+ $osReleaseFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
+ ))) {
+ continue;
+ }
+
+ foreach ($osRelease as $osInfo) {
+ if (false === ($res = @preg_match('/(? 2) {
+ return 'linux';
+ }
+
+ foreach (array(
+ 'fedora' => '/etc/fedora-release',
+ 'centos' => '/etc/centos-release'
+ ) as $distro => $releaseFile) {
+ if (! (false === (
+ $release = @file_get_contents($releaseFile)
+ ) || false === strpos(strtolower($release), $distro))) {
+ return $distro;
+ }
+ }
+
+ if (false !== ($release = @file_get_contents('/etc/redhat-release'))) {
+ $release = strtolower($release);
+ if (false !== strpos($release, 'red hat enterprise linux')) {
+ return 'rhel';
+ }
+ foreach (array('fedora', 'centos') as $distro) {
+ if (false !== strpos($release, $distro)) {
+ return $distro;
+ }
+ }
+ return $reliable < 2 ? 'redhat' : 'linux';
+ }
+
+ if (false !== ($release = @file_get_contents('/etc/SuSE-release'))) {
+ $release = strtolower($release);
+ foreach (array(
+ 'opensuse' => 'opensuse',
+ 'sles' => 'suse linux enterprise server',
+ 'sled' => 'suse linux enterprise desktop'
+ ) as $distro => $name) {
+ if (false !== strpos($release, $name)) {
+ return $distro;
+ }
+ }
+ return $reliable < 2 ? 'suse' : 'linux';
+ }
+
+ if ($reliable < 1) {
+ if (false === ($procVersion = @file_get_contents('/proc/version'))) {
+ return false;
+ }
+ $procVersion = strtolower($procVersion);
+ foreach (array(
+ 'redhat' => 'red hat',
+ 'suse' => 'suse linux',
+ 'ubuntu' => 'ubuntu',
+ 'debian' => 'debian'
+ ) as $distro => $name) {
+ if (false !== strpos($procVersion, $name)) {
+ return $distro;
+ }
+ }
+ }
+
+ return 'linux';
+ }
+
/**
* Test of CLI environment
*
diff --git a/library/Icinga/Application/Version.php b/library/Icinga/Application/Version.php
new file mode 100644
index 000000000..65b6138d9
--- /dev/null
+++ b/library/Icinga/Application/Version.php
@@ -0,0 +1,37 @@
+getApplicationDir() . DIRECTORY_SEPARATOR . 'VERSION'
+ ))) {
+ return false;
+ }
+
+ $matches = array();
+ if (false === ($res = preg_match(
+ '/(?\w+)(?:\s*\(.*?(?:(?<=[\(,])\s*tag\s*:\s*v(?P.+?)\s*(?=[\),]).*?)?\))?\s*(?P\S+)/ms',
+ $appVersion,
+ $matches
+ )) || $res === 0) {
+ return false;
+ }
+
+ foreach ($matches as $key => $value) {
+ if (is_int($key) || $value === '') {
+ unset($matches[$key]);
+ }
+ }
+ return $matches;
+ }
+}
diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php
index 154a33a44..333969891 100644
--- a/library/Icinga/Authentication/User/LdapUserBackend.php
+++ b/library/Icinga/Authentication/User/LdapUserBackend.php
@@ -4,17 +4,16 @@
namespace Icinga\Authentication\User;
use DateTime;
-use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ProgrammingError;
-use Icinga\Repository\Repository;
+use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression;
use Icinga\User;
-class LdapUserBackend extends Repository implements UserBackendInterface
+class LdapUserBackend extends LdapRepository implements UserBackendInterface
{
/**
* The base DN to use for a query
@@ -65,20 +64,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
)
);
- protected $groupOptions;
-
- /**
- * Normed attribute names based on known LDAP environments
- *
- * @var array
- */
- protected $normedAttributes = array(
- 'uid' => 'uid',
- 'user' => 'user',
- 'inetorgperson' => 'inetOrgPerson',
- 'samaccountname' => 'sAMAccountName'
- );
-
/**
* Set the base DN to use for a query
*
@@ -179,34 +164,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
return $this->filter;
}
- public function setGroupOptions(array $options)
- {
- $this->groupOptions = $options;
- return $this;
- }
-
- public function getGroupOptions()
- {
- return $this->groupOptions;
- }
-
- /**
- * Return the given attribute name normed to known LDAP enviroments, if possible
- *
- * @param string $name
- *
- * @return string
- */
- protected function getNormedAttribute($name)
- {
- $loweredName = strtolower($name);
- if (array_key_exists($loweredName, $this->normedAttributes)) {
- return $this->normedAttributes[$loweredName];
- }
-
- return $name;
- }
-
/**
* Apply the given configuration to this backend
*
@@ -325,37 +282,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0;
}
- /**
- * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation
- *
- * @param string|null $value
- *
- * @return int
- */
- protected function retrieveGeneralizedTime($value)
- {
- if ($value === null) {
- return $value;
- }
-
- if (
- ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false
- || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false
- || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false
- || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false
- || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false
- || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false
- ) {
- return $dateTime->getTimeStamp();
- } else {
- Logger::debug(sprintf(
- 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) for user backend "%s".',
- $value,
- $this->getName()
- ));
- }
- }
-
/**
* Return whether the given shadowExpire value defines that a user is permitted to login
*
@@ -413,41 +339,6 @@ class LdapUserBackend extends Repository implements UserBackendInterface
}
}
- /**
- * Retrieve the user groups
- *
- * @TODO: Subject to change, see #7343
- *
- * @param string $dn
- *
- * @return array
- */
- public function getGroups($dn)
- {
- if (empty($this->groupOptions) || ! isset($this->groupOptions['group_base_dn'])) {
- return array();
- }
-
- $result = $this->ds->select()
- ->setBase($this->groupOptions['group_base_dn'])
- ->from(
- $this->groupOptions['group_class'],
- array($this->groupOptions['group_attribute'])
- )
- ->where(
- $this->groupOptions['group_member_attribute'],
- $dn
- )
- ->fetchAll();
-
- $groups = array();
- foreach ($result as $group) {
- $groups[] = $group->{$this->groupOptions['group_attribute']};
- }
-
- return $groups;
- }
-
/**
* Authenticate the given user
*
@@ -472,15 +363,7 @@ class LdapUserBackend extends Repository implements UserBackendInterface
return false;
}
- $authenticated = $this->ds->testCredentials($userDn, $password);
- if ($authenticated) {
- $groups = $this->getGroups($userDn);
- if ($groups !== null) {
- $user->setGroups($groups);
- }
- }
-
- return $authenticated;
+ return $this->ds->testCredentials($userDn, $password);
} catch (LdapException $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php
index 3d11289fb..7afd561a7 100644
--- a/library/Icinga/Authentication/User/UserBackend.php
+++ b/library/Icinga/Authentication/User/UserBackend.php
@@ -3,6 +3,7 @@
namespace Icinga\Authentication\User;
+use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Application\Icinga;
use Icinga\Data\ConfigObject;
@@ -106,8 +107,17 @@ class UserBackend
*
* @throws ConfigurationError
*/
- public static function create($name, ConfigObject $backendConfig)
+ public static function create($name, ConfigObject $backendConfig = null)
{
+ if ($backendConfig === null) {
+ $authConfig = Config::app('authentication');
+ if ($authConfig->hasSection($name)) {
+ $backendConfig = $authConfig->getSection($name);
+ } else {
+ throw new ConfigurationError('User backend "%s" does not exist', $name);
+ }
+ }
+
if ($backendConfig->name !== null) {
$name = $backendConfig->name;
}
@@ -165,12 +175,6 @@ class UserBackend
$backend->setUserClass($backendConfig->get('user_class', 'user'));
$backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName'));
$backend->setFilter($backendConfig->filter);
- $backend->setGroupOptions(array(
- 'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()),
- 'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'),
- 'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'),
- 'group_class' => $backendConfig->get('group_class', 'group')
- ));
break;
case 'ldap':
$backend = new LdapUserBackend($resource);
@@ -178,12 +182,6 @@ class UserBackend
$backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson'));
$backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid'));
$backend->setFilter($backendConfig->filter);
- $backend->setGroupOptions(array(
- 'group_base_dn' => $backendConfig->group_base_dn,
- 'group_attribute' => $backendConfig->group_attribute,
- 'group_member_attribute' => $backendConfig->group_member_attribute,
- 'group_class' => $backendConfig->group_class
- ));
break;
}
diff --git a/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
new file mode 100644
index 000000000..224013e9f
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/LdapUserGroupBackend.php
@@ -0,0 +1,627 @@
+ array(
+ 'order' => 'asc'
+ )
+ );
+
+ /**
+ * Normed attribute names based on known LDAP environments
+ *
+ * @var array
+ */
+ protected $normedAttributes = array(
+ 'uid' => 'uid',
+ 'gid' => 'gid',
+ 'user' => 'user',
+ 'group' => 'group',
+ 'member' => 'member',
+ 'inetorgperson' => 'inetOrgPerson',
+ 'samaccountname' => 'sAMAccountName'
+ );
+
+ /**
+ * The name of this repository
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The datasource being used
+ *
+ * @var Connection
+ */
+ protected $ds;
+
+ /**
+ * Create a new LDAP repository object
+ *
+ * @param Connection $ds The data source to use
+ */
+ public function __construct($ds)
+ {
+ $this->ds = $ds;
+ }
+
+ /**
+ * Return the given attribute name normed to known LDAP enviroments, if possible
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function getNormedAttribute($name)
+ {
+ $loweredName = strtolower($name);
+ if (array_key_exists($loweredName, $this->normedAttributes)) {
+ return $this->normedAttributes[$loweredName];
+ }
+
+ return $name;
+ }
+
+ /**
+ * Set this repository's name
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * Return this repository's name
+ *
+ * In case no name has been explicitly set yet, the class name is returned.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the base DN to use for a user query
+ *
+ * @param string $baseDn
+ *
+ * @return $this
+ */
+ public function setUserBaseDn($baseDn)
+ {
+ if (($baseDn = trim($baseDn))) {
+ $this->userBaseDn = $baseDn;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the base DN to use for a user query
+ *
+ * @return string
+ */
+ public function getUserBaseDn()
+ {
+ return $this->userBaseDn;
+ }
+
+ /**
+ * Set the base DN to use for a group query
+ *
+ * @param string $baseDn
+ *
+ * @return $this
+ */
+ public function setGroupBaseDn($baseDn)
+ {
+ if (($baseDn = trim($baseDn))) {
+ $this->groupBaseDn = $baseDn;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the base DN to use for a group query
+ *
+ * @return string
+ */
+ public function getGroupBaseDn()
+ {
+ return $this->groupBaseDn;
+ }
+
+ /**
+ * Set the objectClass where to look for users
+ *
+ * @param string $userClass
+ *
+ * @return $this
+ */
+ public function setUserClass($userClass)
+ {
+ $this->userClass = $this->getNormedAttribute($userClass);
+ return $this;
+ }
+
+ /**
+ * Return the objectClass where to look for users
+ *
+ * @return string
+ */
+ public function getUserClass()
+ {
+ return $this->userClass;
+ }
+
+ /**
+ * Set the objectClass where to look for groups
+ *
+ * Sets also the base table name for the underlying repository.
+ *
+ * @param string $groupClass
+ *
+ * @return $this
+ */
+ public function setGroupClass($groupClass)
+ {
+ $this->baseTable = $this->groupClass = $this->getNormedAttribute($groupClass);
+ return $this;
+ }
+
+ /**
+ * Return the objectClass where to look for groups
+ *
+ * @return string
+ */
+ public function getGroupClass()
+ {
+ return $this->groupClass;
+ }
+
+ /**
+ * Set the attribute name where to find a user's name
+ *
+ * @param string $userNameAttribute
+ *
+ * @return $this
+ */
+ public function setUserNameAttribute($userNameAttribute)
+ {
+ $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute);
+ return $this;
+ }
+
+ /**
+ * Return the attribute name where to find a user's name
+ *
+ * @return string
+ */
+ public function getUserNameAttribute()
+ {
+ return $this->userNameAttribute;
+ }
+
+ /**
+ * Set the attribute name where to find a group's name
+ *
+ * @param string $groupNameAttribute
+ *
+ * @return $this
+ */
+ public function setGroupNameAttribute($groupNameAttribute)
+ {
+ $this->groupNameAttribute = $this->getNormedAttribute($groupNameAttribute);
+ return $this;
+ }
+
+ /**
+ * Return the attribute name where to find a group's name
+ *
+ * @return string
+ */
+ public function getGroupNameAttribute()
+ {
+ return $this->groupNameAttribute;
+ }
+
+ /**
+ * Set the attribute name where to find a group's member
+ *
+ * @param string $groupMemberAttribute
+ *
+ * @return $this
+ */
+ public function setGroupMemberAttribute($groupMemberAttribute)
+ {
+ $this->groupMemberAttribute = $this->getNormedAttribute($groupMemberAttribute);
+ return $this;
+ }
+
+ /**
+ * Return the attribute name where to find a group's member
+ *
+ * @return string
+ */
+ public function getGroupMemberAttribute()
+ {
+ return $this->groupMemberAttribute;
+ }
+
+ /**
+ * Set the custom LDAP filter to apply on a user query
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setUserFilter($filter)
+ {
+ if (($filter = trim($filter))) {
+ $this->userFilter = $filter;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the custom LDAP filter to apply on a user query
+ *
+ * @return string
+ */
+ public function getUserFilter()
+ {
+ return $this->userFilter;
+ }
+
+ /**
+ * Set the custom LDAP filter to apply on a group query
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setGroupFilter($filter)
+ {
+ if (($filter = trim($filter))) {
+ $this->groupFilter = $filter;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the custom LDAP filter to apply on a group query
+ *
+ * @return string
+ */
+ public function getGroupFilter()
+ {
+ return $this->groupFilter;
+ }
+
+ /**
+ * Return a new query for the given columns
+ *
+ * @param array $columns The desired columns, if null all columns will be queried
+ *
+ * @return RepositoryQuery
+ */
+ public function select(array $columns = null)
+ {
+ $query = parent::select($columns);
+ $query->getQuery()->setBase($this->groupBaseDn);
+ if ($this->groupFilter) {
+ // TODO(jom): This should differentiate between groups and their memberships
+ $query->getQuery()->where(new Expression($this->groupFilter));
+ }
+
+ return $query;
+ }
+
+ /**
+ * Initialize this repository's query columns
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case either $this->groupNameAttribute or $this->groupClass has not been set yet
+ */
+ protected function initializeQueryColumns()
+ {
+ if ($this->groupClass === null) {
+ throw new ProgrammingError('It is required to set the objectClass where to look for groups first');
+ }
+ if ($this->groupNameAttribute === null) {
+ throw new ProgrammingError('It is required to set a attribute name where to find a group\'s name first');
+ }
+
+ if ($this->ds->getCapabilities()->hasAdOid()) {
+ $createdAtAttribute = 'whenCreated';
+ $lastModifiedAttribute = 'whenChanged';
+ } else {
+ $createdAtAttribute = 'createTimestamp';
+ $lastModifiedAttribute = 'modifyTimestamp';
+ }
+
+ // TODO(jom): Fetching memberships does not work currently, we'll need some aggregate functionality!
+ $columns = array(
+ 'group' => $this->groupNameAttribute,
+ 'group_name' => $this->groupNameAttribute,
+ 'user' => $this->groupMemberAttribute,
+ 'user_name' => $this->groupMemberAttribute,
+ 'created_at' => $createdAtAttribute,
+ 'last_modified' => $lastModifiedAttribute
+ );
+ return array('group' => $columns, 'group_membership' => $columns);
+ }
+
+ /**
+ * Initialize this repository's conversion rules
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $this->groupClass has not been set yet
+ */
+ protected function initializeConversionRules()
+ {
+ if ($this->groupClass === null) {
+ throw new ProgrammingError('It is required to set the objectClass where to look for groups first');
+ }
+
+ return array(
+ $this->groupClass => array(
+ 'created_at' => 'generalized_time',
+ 'last_modified' => 'generalized_time'
+ )
+ );
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * This will return $this->groupClass in case $table equals "group" or "group_membership".
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $table = parent::requireTable($table, $query);
+ if ($table === 'group' || $table === 'group_membership') {
+ $table = $this->groupClass;
+ }
+
+ return $table;
+ }
+
+ /**
+ * Return the groups the given user is a member of
+ *
+ * @param User $user
+ *
+ * @return array
+ */
+ public function getMemberships(User $user)
+ {
+ $userQuery = $this->ds
+ ->select()
+ ->from($this->userClass)
+ ->where($this->userNameAttribute, $user->getUsername())
+ ->setBase($this->userBaseDn)
+ ->setUsePagedResults(false);
+ if ($this->userFilter) {
+ $userQuery->where(new Expression($this->userFilter));
+ }
+
+ if (($userDn = $userQuery->fetchDn()) === null) {
+ return array();
+ }
+
+ $groupQuery = $this->ds
+ ->select()
+ ->from($this->groupClass, array($this->groupNameAttribute))
+ ->where($this->groupMemberAttribute, $userDn)
+ ->setBase($this->groupBaseDn);
+ if ($this->groupFilter) {
+ $groupQuery->where(new Expression($this->groupFilter));
+ }
+
+ $groups = array();
+ foreach ($groupQuery as $row) {
+ $groups[] = $row->{$this->groupNameAttribute};
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Apply the given configuration on this backend
+ *
+ * @param ConfigObject $config
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case a linked user backend does not exist or is invalid
+ */
+ public function setConfig(ConfigObject $config)
+ {
+ if ($config->backend === 'ldap') {
+ $defaults = $this->getOpenLdapDefaults();
+ } elseif ($config->backend === 'msldap') {
+ $defaults = $this->getActiveDirectoryDefaults();
+ } else {
+ $defaults = new ConfigObject();
+ }
+
+ if ($config->user_backend && $config->user_backend !== 'none') {
+ $userBackend = UserBackend::create($config->user_backend);
+ if (! $userBackend instanceof LdapUserBackend) {
+ throw new ConfigurationError('User backend "%s" is not of type LDAP', $config->user_backend);
+ }
+
+ if (
+ $this->ds->getHostname() !== $userBackend->getDataSource()->getHostname()
+ || $this->ds->getPort() !== $userBackend->getDataSource()->getPort()
+ ) {
+ // TODO(jom): Elaborate whether it makes sense to link directories on different hosts
+ throw new ConfigurationError(
+ 'It is required that a linked user backend refers to the '
+ . 'same directory as it\'s user group backend counterpart'
+ );
+ }
+
+ $defaults->merge(array(
+ 'user_base_dn' => $userBackend->getBaseDn(),
+ 'user_class' => $userBackend->getUserClass(),
+ 'user_name_attribute' => $userBackend->getUserNameAttribute(),
+ 'user_filter' => $userBackend->getFilter()
+ ));
+ }
+
+ return $this
+ ->setGroupBaseDn($config->base_dn)
+ ->setUserBaseDn($config->get('user_base_dn', $this->getGroupBaseDn()))
+ ->setGroupClass($config->get('group_class', $defaults->group_class))
+ ->setUserClass($config->get('user_class', $defaults->user_class))
+ ->setGroupNameAttribute($config->get('group_name_attribute', $defaults->group_name_attribute))
+ ->setUserNameAttribute($config->get('user_name_attribute', $defaults->user_name_attribute))
+ ->setGroupMemberAttribute($config->get('group_member_attribute', $defaults->group_member_attribute))
+ ->setGroupFilter($config->filter)
+ ->setUserFilter($config->user_filter);
+ }
+
+ /**
+ * Return the configuration defaults for an OpenLDAP environment
+ *
+ * @return ConfigObject
+ */
+ public function getOpenLdapDefaults()
+ {
+ return new ConfigObject(array(
+ 'group_class' => 'group',
+ 'user_class' => 'inetOrgPerson',
+ 'group_name_attribute' => 'gid',
+ 'user_name_attribute' => 'uid',
+ 'group_member_attribute' => 'member'
+ ));
+ }
+
+ /**
+ * Return the configuration defaults for an ActiveDirectory environment
+ *
+ * @return ConfigObject
+ */
+ public function getActiveDirectoryDefaults()
+ {
+ return new ConfigObject(array(
+ 'group_class' => 'group',
+ 'user_class' => 'user',
+ 'group_name_attribute' => 'sAMAccountName',
+ 'user_name_attribute' => 'sAMAccountName',
+ 'group_member_attribute' => 'member'
+ ));
+ }
+}
diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
index dd4900ea8..978860a37 100644
--- a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
+++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
@@ -21,6 +21,8 @@ class UserGroupBackend
*/
protected static $defaultBackends = array(
'db',
+ 'ldap',
+ 'msldap',
//'ini'
);
@@ -156,6 +158,11 @@ class UserGroupBackend
case 'ini':
$backend = new IniUserGroupBackend($resource);
break;
+ case 'ldap':
+ case 'msldap':
+ $backend = new LdapUserGroupBackend($resource);
+ $backend->setConfig($backendConfig);
+ break;
}
$backend->setName($name);
diff --git a/library/Icinga/File/Ini/IniEditor.php b/library/Icinga/File/Ini/IniEditor.php
index 1f8202d9d..00bea8df4 100644
--- a/library/Icinga/File/Ini/IniEditor.php
+++ b/library/Icinga/File/Ini/IniEditor.php
@@ -313,14 +313,19 @@ class IniEditor
*/
public function getText()
{
- $this->cleanUpWhitespaces();
- return rtrim(implode(PHP_EOL, $this->text)) . PHP_EOL;
+ $this->normalizeSectionSpacing();
+
+ // trim leading and trailing whitespaces from generated file
+ $txt = trim(implode(PHP_EOL, $this->text)) . PHP_EOL;
+
+ // replace linebreaks, unless they preceed a comment or a section
+ return preg_replace("/\n[\n]*([^;\[])/", "\n$1", $txt);
}
/**
- * Remove all unneeded line breaks between sections
+ * normalize section spacing according to the current settings
*/
- private function cleanUpWhitespaces()
+ private function normalizeSectionSpacing()
{
$i = count($this->text) - 1;
for (; $i > 0; $i--) {
@@ -328,24 +333,18 @@ class IniEditor
if ($this->isSectionDeclaration($line) && $i > 0) {
$i--;
$line = $this->text[$i];
- /*
- * Ignore comments that are glued to the section declaration
- */
+ // ignore comments that are glued to the section declaration
while ($i > 0 && $this->isComment($line)) {
$i--;
$line = $this->text[$i];
}
- /*
- * Remove whitespaces between the sections
- */
+ // remove whitespaces between the sections
while ($i > 0 && preg_match('/^\s*$/', $line) === 1) {
$this->deleteLine($i);
$i--;
$line = $this->text[$i];
}
- /*
- * Refresh section separators
- */
+ // refresh section separators
if ($i !== 0 && $this->sectionSeparators > 0) {
$this->insertAtLine($i + 1, str_repeat(PHP_EOL, $this->sectionSeparators - 1));
}
@@ -621,6 +620,6 @@ class IniEditor
private function sanitize($value)
{
- return str_replace('\n', '', $value);
+ return str_replace("\n", '', $value);
}
}
diff --git a/library/Icinga/File/Ini/IniWriter.php b/library/Icinga/File/Ini/IniWriter.php
index e43c226aa..5b565a6a0 100644
--- a/library/Icinga/File/Ini/IniWriter.php
+++ b/library/Icinga/File/Ini/IniWriter.php
@@ -60,7 +60,7 @@ class IniWriter extends Zend_Config_Writer_FileAbstract
{
if (file_exists($this->_filename)) {
$oldconfig = new Zend_Config_Ini($this->_filename);
- $content = file_get_contents($this->_filename);
+ $content = trim(file_get_contents($this->_filename));
} else {
$oldconfig = new Zend_Config(array());
$content = '';
diff --git a/library/Icinga/Repository/LdapRepository.php b/library/Icinga/Repository/LdapRepository.php
new file mode 100644
index 000000000..7cf00ae66
--- /dev/null
+++ b/library/Icinga/Repository/LdapRepository.php
@@ -0,0 +1,66 @@
+
+ * Attribute name normalization
+ *
+ */
+abstract class LdapRepository extends Repository
+{
+ /**
+ * The datasource being used
+ *
+ * @var Connection
+ */
+ protected $ds;
+
+ /**
+ * Normed attribute names based on known LDAP environments
+ *
+ * @var array
+ */
+ protected $normedAttributes = array(
+ 'uid' => 'uid',
+ 'gid' => 'gid',
+ 'user' => 'user',
+ 'group' => 'group',
+ 'member' => 'member',
+ 'inetorgperson' => 'inetOrgPerson',
+ 'samaccountname' => 'sAMAccountName'
+ );
+
+ /**
+ * Create a new LDAP repository object
+ *
+ * @param Connection $ds The data source to use
+ */
+ public function __construct(Connection $ds)
+ {
+ parent::__construct($ds);
+ }
+
+ /**
+ * Return the given attribute name normed to known LDAP enviroments, if possible
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ protected function getNormedAttribute($name)
+ {
+ $loweredName = strtolower($name);
+ if (array_key_exists($loweredName, $this->normedAttributes)) {
+ return $this->normedAttributes[$loweredName];
+ }
+
+ return $name;
+ }
+}
\ No newline at end of file
diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
index 803f4958f..98b3e0ab4 100644
--- a/library/Icinga/Repository/Repository.php
+++ b/library/Icinga/Repository/Repository.php
@@ -599,6 +599,37 @@ abstract class Repository implements Selectable
return $value;
}
+ /**
+ * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation
+ *
+ * @param string|null $value
+ *
+ * @return int
+ */
+ protected function retrieveGeneralizedTime($value)
+ {
+ if ($value === null) {
+ return $value;
+ }
+
+ if (
+ ($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false
+ || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false
+ || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false
+ || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false
+ || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false
+ || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false
+ ) {
+ return $dateTime->getTimeStamp();
+ } else {
+ Logger::debug(sprintf(
+ 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".',
+ $value,
+ $this->getName()
+ ));
+ }
+ }
+
/**
* Validate that the requested table exists
*
diff --git a/library/Icinga/Web/Form.php b/library/Icinga/Web/Form.php
index 48f7acdb7..08cb02388 100644
--- a/library/Icinga/Web/Form.php
+++ b/library/Icinga/Web/Form.php
@@ -855,8 +855,19 @@ class Form extends Zend_Form
public function populate(array $defaults)
{
$this->create($defaults);
+ $this->preserveDefaults($this, $defaults);
+ return parent::populate($defaults);
+ }
- foreach ($this->getElements() as $name => $_) {
+ /**
+ * Recurse the given form and unset all unchanged default values
+ *
+ * @param Zend_Form $form
+ * @param array $defaults
+ */
+ protected function preserveDefaults(Zend_Form $form, array & $defaults)
+ {
+ foreach ($form->getElements() as $name => $_) {
if (
array_key_exists($name, $defaults)
&& array_key_exists($name . static::DEFAULT_SUFFIX, $defaults)
@@ -866,7 +877,9 @@ class Form extends Zend_Form
}
}
- return parent::populate($defaults);
+ foreach ($form->getSubForms() as $_ => $subForm) {
+ $this->preserveDefaults($subForm, $defaults);
+ }
}
/**
diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php
index 63d92fc9f..45dd2dd9c 100644
--- a/library/Icinga/Web/Menu.php
+++ b/library/Icinga/Web/Menu.php
@@ -279,6 +279,11 @@ class Menu implements RecursiveIterator
'priority' => 990,
'renderer' => 'ForeignMenuItemRenderer'
));
+
+ $this->add(t('About'), array(
+ 'url' => 'about',
+ 'priority' => 1000
+ ));
}
}
diff --git a/library/Icinga/Web/View/helpers/url.php b/library/Icinga/Web/View/helpers/url.php
index 39e8de951..06ace7f19 100644
--- a/library/Icinga/Web/View/helpers/url.php
+++ b/library/Icinga/Web/View/helpers/url.php
@@ -20,7 +20,12 @@ $this->addHelperFunction('url', function ($path = null, $params = null) {
} else {
$url = Url::fromPath($path);
}
+
if ($params !== null) {
+ if ($url === $path) {
+ $url = clone $url;
+ }
+
$url->overwriteParams($params);
}
diff --git a/modules/doc/application/views/scripts/search/index.phtml b/modules/doc/application/views/scripts/search/index.phtml
index 3311c47a1..c613f04df 100644
--- a/modules/doc/application/views/scripts/search/index.phtml
+++ b/modules/doc/application/views/scripts/search/index.phtml
@@ -1,8 +1,8 @@
$search): ?>
- isEmpty()): ?>
-
= $this->escape($title) ?>
- = $search ?>
-
+ = $this->escape($title) ?>
+ = $search->isEmpty()
+ ? $this->translate('No documentation found matching the filter')
+ : $search ?>
diff --git a/modules/monitoring/application/views/scripts/timeline/index.phtml b/modules/monitoring/application/views/scripts/timeline/index.phtml
index d0cc8d34c..209eb0f1b 100644
--- a/modules/monitoring/application/views/scripts/timeline/index.phtml
+++ b/modules/monitoring/application/views/scripts/timeline/index.phtml
@@ -132,8 +132,8 @@ $extrapolatedCircleWidth = $timeline->getExtrapolatedCircleWidth($timeInfo[1][$g
= $this->translate('Welcome to the configuration of Icinga Web 2!') ?>
@@ -51,6 +84,12 @@ $cliPath = realpath(Icinga::app()->getApplicationDir() . '/../bin/icingacli');
= $this->translate('Your webserver\'s user is a member of the system group "icingaweb2"'); ?>
+
+
+ = $this->escape($groupadd . ';') ?>
+ = $this->escape($usermod . ';') ?>
+
+
= $this->translate('If you\'ve got the IcingaCLI installed you can do the following:'); ?>
= $cliPath ? $cliPath : 'icingacli'; ?> setup config directory --group icingaweb2= $configDir !== '/etc/icingaweb2' ? ' --config ' . $configDir : ''; ?>;
diff --git a/public/css/icinga/layout-structure.less b/public/css/icinga/layout-structure.less
index 3a8713a38..5b5eee5ef 100644
--- a/public/css/icinga/layout-structure.less
+++ b/public/css/icinga/layout-structure.less
@@ -331,8 +331,7 @@ html {
position: absolute;
}
-/* TODO: replace this with .error */
-.fileNotReadable {
+.message-error {
padding: 0.5em;
background-color: @colorCritical;
font-weight: bold;
diff --git a/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
index f7373a7ae..6fe63adf4 100644
--- a/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
+++ b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
@@ -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);
}
}
diff --git a/test/php/library/Icinga/File/Ini/IniWriterTest.php b/test/php/library/Icinga/File/Ini/IniWriterTest.php
index 2d9659675..fa9ad895f 100644
--- a/test/php/library/Icinga/File/Ini/IniWriterTest.php
+++ b/test/php/library/Icinga/File/Ini/IniWriterTest.php
@@ -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
*