diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php
index 7d492f8c1..4d6fe9928 100644
--- a/application/controllers/AuthenticationController.php
+++ b/application/controllers/AuthenticationController.php
@@ -7,7 +7,7 @@ use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Authentication\AuthChain;
-use Icinga\Authentication\Backend\ExternalBackend;
+use Icinga\Authentication\User\ExternalBackend;
use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError;
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
index 5df2be008..9d55910e9 100644
--- a/application/controllers/ConfigController.php
+++ b/application/controllers/ConfigController.php
@@ -5,14 +5,15 @@ use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Application\Modules\Module;
use Icinga\Data\ResourceFactory;
-use Icinga\Forms\Config\AuthenticationBackendConfigForm;
-use Icinga\Forms\Config\AuthenticationBackendReorderForm;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackendReorderForm;
use Icinga\Forms\Config\GeneralConfigForm;
use Icinga\Forms\Config\ResourceConfigForm;
use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Security\SecurityException;
use Icinga\Web\Controller;
use Icinga\Web\Notification;
+use Icinga\Web\Url;
use Icinga\Web\Widget;
/**
@@ -38,20 +39,12 @@ class ConfigController extends Controller
$auth = $this->Auth();
$allowedActions = array();
if ($auth->hasPermission('config/application/general')) {
- $tabs->add('application', array(
+ $tabs->add('general', array(
'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
- 'label' => $this->translate('Application'),
- 'url' => 'config/application'
+ 'label' => $this->translate('General'),
+ 'url' => 'config/general'
));
- $allowedActions[] = 'application';
- }
- if ($auth->hasPermission('config/application/authentication')) {
- $tabs->add('authentication', array(
- 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
- 'label' => $this->translate('Authentication'),
- 'url' => 'config/authentication'
- ));
- $allowedActions[] = 'authentication';
+ $allowedActions[] = 'general';
}
if ($auth->hasPermission('config/application/resources')) {
$tabs->add('resource', array(
@@ -61,15 +54,21 @@ class ConfigController extends Controller
));
$allowedActions[] = 'resource';
}
- if ($auth->hasPermission('config/application/roles')) {
- $tabs->add('roles', array(
- 'title' => $this->translate(
- 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
- ),
- 'label' => $this->translate('Roles'),
- 'url' => 'roles'
+ if ($auth->hasPermission('config/application/userbackend')) {
+ $tabs->add('userbackend', array(
+ 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
+ 'label' => $this->translate('Authentication'),
+ 'url' => 'config/userbackend'
));
- $allowedActions[] = 'roles';
+ $allowedActions[] = 'userbackend';
+ }
+ if ($auth->hasPermission('config/application/usergroupbackend')) {
+ $tabs->add('usergroupbackend', array(
+ 'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'usergroupbackend/list'
+ ));
+ $allowedActions[] = 'usergroupbackend';
}
$this->firstAllowedAction = array_shift($allowedActions);
}
@@ -85,7 +84,7 @@ class ConfigController extends Controller
public function indexAction()
{
if ($this->firstAllowedAction === null) {
- throw new SecurityException($this->translate('No permission for configuration'));
+ throw new SecurityException($this->translate('No permission for application configuration'));
}
$action = $this->getTabs()->get($this->firstAllowedAction);
if (substr($action->getUrl()->getPath(), 0, 7) === 'config/') {
@@ -96,11 +95,11 @@ class ConfigController extends Controller
}
/**
- * Application configuration
+ * General configuration
*
- * @throws SecurityException If the user lacks the permission for configuring the application
+ * @throws SecurityException If the user lacks the permission for configuring the general configuration
*/
- public function applicationAction()
+ public function generalAction()
{
$this->assertPermission('config/application/general');
$form = new GeneralConfigForm();
@@ -108,7 +107,7 @@ class ConfigController extends Controller
$form->handleRequest();
$this->view->form = $form;
- $this->view->tabs->activate('application');
+ $this->view->tabs->activate('general');
}
/**
@@ -201,71 +200,72 @@ class ConfigController extends Controller
}
/**
- * Action for listing and reordering authentication backends
+ * Action for listing and reordering user backends
*/
- public function authenticationAction()
+ public function userbackendAction()
{
- $this->assertPermission('config/application/authentication');
- $form = new AuthenticationBackendReorderForm();
+ $this->assertPermission('config/application/userbackend');
+ $form = new UserBackendReorderForm();
$form->setIniConfig(Config::app('authentication'));
$form->handleRequest();
$this->view->form = $form;
- $this->view->tabs->activate('authentication');
- $this->render('authentication/reorder');
+ $this->view->tabs->activate('userbackend');
+ $this->render('userbackend/reorder');
}
/**
- * Action for creating a new authentication backend
+ * Action for creating a new user backend
*/
- public function createauthenticationbackendAction()
+ public function createuserbackendAction()
{
- $this->assertPermission('config/application/authentication');
- $form = new AuthenticationBackendConfigForm();
- $form->setTitle($this->translate('Create New Authentication Backend'));
+ $this->assertPermission('config/application/userbackend');
+ $form = new UserBackendConfigForm();
+ $form->setTitle($this->translate('Create New User Backend'));
$form->addDescription($this->translate(
'Create a new backend for authenticating your users. This backend'
. ' will be added at the end of your authentication order.'
));
$form->setIniConfig(Config::app('authentication'));
$form->setResourceConfig(ResourceFactory::getResourceConfigs());
- $form->setRedirectUrl('config/authentication');
+ $form->setRedirectUrl('config/userbackend');
$form->handleRequest();
$this->view->form = $form;
- $this->view->tabs->activate('authentication');
- $this->render('authentication/create');
+ $this->view->tabs->activate('userbackend');
+ $this->render('userbackend/create');
}
/**
- * Action for editing authentication backends
+ * Action for editing user backends
*/
- public function editauthenticationbackendAction()
+ public function edituserbackendAction()
{
- $this->assertPermission('config/application/authentication');
- $form = new AuthenticationBackendConfigForm();
- $form->setTitle($this->translate('Edit Backend'));
+ $this->assertPermission('config/application/userbackend');
+ $form = new UserBackendConfigForm();
+ $form->setTitle($this->translate('Edit User Backend'));
$form->setIniConfig(Config::app('authentication'));
$form->setResourceConfig(ResourceFactory::getResourceConfigs());
- $form->setRedirectUrl('config/authentication');
+ $form->setRedirectUrl('config/userbackend');
+ $form->setAction(Url::fromRequest());
$form->handleRequest();
$this->view->form = $form;
- $this->view->tabs->activate('authentication');
- $this->render('authentication/modify');
+ $this->view->tabs->activate('userbackend');
+ $this->render('userbackend/modify');
}
/**
- * Action for removing a backend from the authentication list
+ * Action for removing a user backend
*/
- public function removeauthenticationbackendAction()
+ public function removeuserbackendAction()
{
- $this->assertPermission('config/application/authentication');
+ $this->assertPermission('config/application/userbackend');
$form = new ConfirmRemovalForm(array(
'onSuccess' => function ($form) {
- $configForm = new AuthenticationBackendConfigForm();
+ $configForm = new UserBackendConfigForm();
$configForm->setIniConfig(Config::app('authentication'));
- $authBackend = $form->getRequest()->getQuery('auth_backend');
+ $authBackend = $form->getRequest()->getQuery('backend');
try {
$configForm->remove($authBackend);
@@ -276,7 +276,7 @@ class ConfigController extends Controller
if ($configForm->save()) {
Notification::success(sprintf(
- t('Authentication backend "%s" has been successfully removed'),
+ t('User backend "%s" has been successfully removed'),
$authBackend
));
} else {
@@ -284,13 +284,14 @@ class ConfigController extends Controller
}
}
));
- $form->setTitle($this->translate('Remove Backend'));
- $form->setRedirectUrl('config/authentication');
+ $form->setTitle($this->translate('Remove User Backend'));
+ $form->setRedirectUrl('config/userbackend');
+ $form->setAction(Url::fromRequest());
$form->handleRequest();
$this->view->form = $form;
- $this->view->tabs->activate('authentication');
- $this->render('authentication/remove');
+ $this->view->tabs->activate('userbackend');
+ $this->render('userbackend/remove');
}
/**
@@ -373,7 +374,7 @@ class ConfigController extends Controller
if ($config->get('resource') === $resource) {
$form->addDescription(sprintf(
$this->translate(
- 'The resource "%s" is currently in use by the authentication backend "%s". ' .
+ 'The resource "%s" is currently utilized for authentication by user backend "%s". ' .
'Removing the resource can result in noone being able to log in any longer.'
),
$resource,
diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php
new file mode 100644
index 000000000..70954aaf1
--- /dev/null
+++ b/application/controllers/GroupController.php
@@ -0,0 +1,345 @@
+assertPermission('config/authentication/groups/show');
+ $backendNames = array_map(
+ function ($b) { return $b->getName(); },
+ $this->loadUserGroupBackends('Icinga\Data\Selectable')
+ );
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('Usergroup Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserGroupBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('group_name'));
+ $filterEditor = Widget::create('filterEditor')
+ ->setQuery($query)
+ ->preserveParams('limit', 'sort', 'dir', 'view', 'backend')
+ ->ignoreParams('page')
+ ->handleRequest($this->getRequest());
+ $query->applyFilter($filterEditor->getFilter());
+ $this->setupFilterControl($filterEditor);
+
+ $this->view->groups = $query;
+ $this->view->backend = $backend;
+ $this->createListTabs()->activate('group/list');
+
+ $this->setupPaginationControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('Group'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a group
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/authentication/groups/show');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'));
+
+ $group = $backend->select(array(
+ 'group_name',
+ 'created_at',
+ 'last_modified'
+ ))->where('group_name', $groupName)->fetchRow();
+ if ($group === false) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $members = $backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $groupName);
+
+ $filterEditor = Widget::create('filterEditor')
+ ->setQuery($members)
+ ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'group')
+ ->ignoreParams('page')
+ ->handleRequest($this->getRequest());
+ $members->applyFilter($filterEditor->getFilter());
+
+ $this->setupFilterControl($filterEditor);
+ $this->setupPaginationControl($members);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $members
+ );
+
+ $this->view->group = $group;
+ $this->view->backend = $backend;
+ $this->view->members = $members;
+ $this->createShowTabs($backend->getName(), $groupName)->activate('group/show');
+
+ if ($this->hasPermission('config/authentication/groups/edit') && $backend instanceof Reducible) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->setAction(
+ Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('group/show', array(
+ 'backend' => $backend->getName(),
+ 'group' => $groupName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-like',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Remove this member')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+ }
+
+ /**
+ * Add a group
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/authentication/groups/add');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Edit a group
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/authentication/groups/edit');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ );
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Remove a group
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/authentication/groups/remove');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserGroupForm();
+ $form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($groupName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Add a group member
+ */
+ public function addmemberAction()
+ {
+ $this->assertPermission('config/authentication/groups/edit');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+
+ $form = new AddMemberForm();
+ $form->setDataSource($this->fetchUsers())
+ ->setBackend($backend)
+ ->setGroupName($groupName)
+ ->setRedirectUrl(
+ Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
+ )
+ ->setUidDisabled();
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Remove a group member
+ */
+ public function removememberAction()
+ {
+ $this->assertPermission('config/authentication/groups/edit');
+ $this->assertHttpMethod('POST');
+ $groupName = $this->params->getRequired('group');
+ $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new Form(array(
+ 'onSuccess' => function ($form) use ($groupName, $backend) {
+ foreach ($form->getValue('user_name') as $userName) {
+ try {
+ $backend->delete(
+ 'group_membership',
+ Filter::matchAll(
+ Filter::where('group_name', $groupName),
+ Filter::where('user_name', $userName)
+ )
+ );
+ Notification::success(sprintf(
+ t('User "%s" has been removed from group "%s"'),
+ $userName,
+ $groupName
+ ));
+ } catch (NotFoundError $e) {
+ throw $e;
+ } catch (Exception $e) {
+ Notification::error($e->getMessage());
+ }
+ }
+
+ $redirect = $form->getValue('redirect');
+ if (! empty($redirect)) {
+ $form->setRedirectUrl(htmlspecialchars_decode($redirect));
+ }
+
+ return true;
+ }
+ ));
+ $form->setUidDisabled();
+ $form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
+ $form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true));
+ $form->addElement('hidden', 'redirect');
+
+ try {
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
+ }
+ }
+
+ /**
+ * Fetch and return all users from all user backends
+ *
+ * @return ArrayDatasource
+ */
+ protected function fetchUsers()
+ {
+ $users = array();
+ foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) {
+ try {
+ foreach ($backend->select(array('user_name')) as $row) {
+ $users[] = $row;
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any users from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($users);
+ }
+
+ /**
+ * Create the tabs to display when showing a group
+ *
+ * @param string $backendName
+ * @param string $groupName
+ */
+ protected function createShowTabs($backendName, $groupName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'group/show',
+ array(
+ 'title' => sprintf($this->translate('Show group %s'), $groupName),
+ 'label' => $this->translate('Group'),
+ 'icon' => 'users',
+ 'url' => Url::fromPath('group/show', array('backend' => $backendName, 'group' => $groupName))
+ )
+ );
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/RolesController.php b/application/controllers/RoleController.php
similarity index 68%
rename from application/controllers/RolesController.php
rename to application/controllers/RoleController.php
index d4b7b7c63..0384009ec 100644
--- a/application/controllers/RolesController.php
+++ b/application/controllers/RoleController.php
@@ -4,69 +4,27 @@
use Icinga\Application\Config;
use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Forms\Security\RoleForm;
-use Icinga\Web\Controller\ActionController;
+use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\Notification;
-use Icinga\Web\Widget;
-/**
- * Roles configuration
- */
-class RolesController extends ActionController
+class RoleController extends AuthBackendController
{
- /**
- * Initialize tabs and validate the user's permissions
- *
- * @throws \Icinga\Security\SecurityException If the user lacks permissions for configuring roles
- */
- public function init()
- {
- $this->assertPermission('config/application/roles');
- $tabs = $this->getTabs();
- $auth = $this->Auth();
- if ($auth->hasPermission('config/application/general')) {
- $tabs->add('application', array(
- 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
- 'label' => $this->translate('Application'),
- 'url' => 'config'
- ));
- }
- if ($auth->hasPermission('config/application/authentication')) {
- $tabs->add('authentication', array(
- 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
- 'label' => $this->translate('Authentication'),
- 'url' => 'config/authentication'
- ));
- }
- if ($auth->hasPermission('config/application/resources')) {
- $tabs->add('resource', array(
- 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'),
- 'label' => $this->translate('Resources'),
- 'url' => 'config/resource'
- ));
- }
- $tabs->add('roles', array(
- 'title' => $this->translate(
- 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
- ),
- 'label' => $this->translate('Roles'),
- 'url' => 'roles'
- ));
- }
-
/**
* List roles
*/
- public function indexAction()
+ public function listAction()
{
- $this->view->tabs->activate('roles');
+ $this->assertPermission('config/authentication/roles/show');
+ $this->createListTabs()->activate('role/list');
$this->view->roles = Config::app('roles', true);
}
/**
* Create a new role
*/
- public function newAction()
+ public function addAction()
{
+ $this->assertPermission('config/authentication/roles/add');
$role = new RoleForm(array(
'onSuccess' => function (RoleForm $role) {
$name = $role->getElement('name')->getValue();
@@ -88,9 +46,10 @@ class RolesController extends ActionController
->setTitle($this->translate('New Role'))
->setSubmitLabel($this->translate('Create Role'))
->setIniConfig(Config::app('roles', true))
- ->setRedirectUrl('roles')
+ ->setRedirectUrl('role/list')
->handleRequest();
$this->view->form = $role;
+ $this->render('form');
}
/**
@@ -98,8 +57,9 @@ class RolesController extends ActionController
*
* @throws Zend_Controller_Action_Exception If the required parameter 'role' is missing or the role does not exist
*/
- public function updateAction()
+ public function editAction()
{
+ $this->assertPermission('config/authentication/roles/edit');
$name = $this->_request->getParam('role');
if (empty($name)) {
throw new Zend_Controller_Action_Exception(
@@ -137,9 +97,10 @@ class RolesController extends ActionController
}
return false;
})
- ->setRedirectUrl('roles')
+ ->setRedirectUrl('role/list')
->handleRequest();
$this->view->form = $role;
+ $this->render('form');
}
/**
@@ -149,6 +110,7 @@ class RolesController extends ActionController
*/
public function removeAction()
{
+ $this->assertPermission('config/authentication/roles/remove');
$name = $this->_request->getParam('role');
if (empty($name)) {
throw new Zend_Controller_Action_Exception(
@@ -185,8 +147,9 @@ class RolesController extends ActionController
$confirmation
->setTitle(sprintf($this->translate('Remove Role %s'), $name))
->setSubmitLabel($this->translate('Remove Role'))
- ->setRedirectUrl('roles')
+ ->setRedirectUrl('role/list')
->handleRequest();
$this->view->form = $confirmation;
+ $this->render('form');
}
}
diff --git a/application/controllers/UserController.php b/application/controllers/UserController.php
new file mode 100644
index 000000000..c71fe9696
--- /dev/null
+++ b/application/controllers/UserController.php
@@ -0,0 +1,304 @@
+assertPermission('config/authentication/users/show');
+ $backendNames = array_map(
+ function ($b) { return $b->getName(); },
+ $this->loadUserBackends('Icinga\Data\Selectable')
+ );
+ $this->view->backendSelection = new Form();
+ $this->view->backendSelection->setAttrib('class', 'backend-selection');
+ $this->view->backendSelection->setUidDisabled();
+ $this->view->backendSelection->setMethod('GET');
+ $this->view->backendSelection->setTokenDisabled();
+ $this->view->backendSelection->addElement(
+ 'select',
+ 'backend',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('Authentication Backend'),
+ 'multiOptions' => array_combine($backendNames, $backendNames),
+ 'value' => $this->params->get('backend')
+ )
+ );
+
+ $backend = $this->getUserBackend($this->params->get('backend'));
+ if ($backend === null) {
+ $this->view->backend = null;
+ return;
+ }
+
+ $query = $backend->select(array('user_name'));
+ $filterEditor = Widget::create('filterEditor')
+ ->setQuery($query)
+ ->preserveParams('limit', 'sort', 'dir', 'view', 'backend')
+ ->ignoreParams('page')
+ ->handleRequest($this->getRequest());
+ $query->applyFilter($filterEditor->getFilter());
+ $this->setupFilterControl($filterEditor);
+
+ $this->view->users = $query;
+ $this->view->backend = $backend;
+ $this->createListTabs()->activate('user/list');
+
+ $this->setupPaginationControl($query);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'user_name' => $this->translate('Username'),
+ 'is_active' => $this->translate('Active'),
+ 'created_at' => $this->translate('Created at'),
+ 'last_modified' => $this->translate('Last modified')
+ ),
+ $query
+ );
+ }
+
+ /**
+ * Show a user
+ */
+ public function showAction()
+ {
+ $this->assertPermission('config/authentication/users/show');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ $user = $backend->select(array(
+ 'user_name',
+ 'is_active',
+ 'created_at',
+ 'last_modified'
+ ))->where('user_name', $userName)->fetchRow();
+ if ($user === false) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $memberships = $this->loadMemberships(new User($userName))->select();
+
+ $filterEditor = Widget::create('filterEditor')
+ ->setQuery($memberships)
+ ->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'user')
+ ->ignoreParams('page')
+ ->handleRequest($this->getRequest());
+ $memberships->applyFilter($filterEditor->getFilter());
+
+ $this->setupFilterControl($filterEditor);
+ $this->setupPaginationControl($memberships);
+ $this->setupLimitControl();
+ $this->setupSortControl(
+ array(
+ 'group_name' => $this->translate('Group')
+ ),
+ $memberships
+ );
+
+ if ($this->hasPermission('config/authentication/groups/edit')) {
+ $extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ $this->view->showCreateMembershipLink = ! empty($extensibleBackends);
+ } else {
+ $this->view->showCreateMembershipLink = false;
+ }
+
+ $this->view->user = $user;
+ $this->view->backend = $backend;
+ $this->view->memberships = $memberships;
+ $this->createShowTabs($backend->getName(), $userName)->activate('user/show');
+
+ if ($this->hasPermission('config/authentication/groups/edit')) {
+ $removeForm = new Form();
+ $removeForm->setUidDisabled();
+ $removeForm->addElement('hidden', 'user_name', array(
+ 'isArray' => true,
+ 'value' => $userName,
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('hidden', 'redirect', array(
+ 'value' => Url::fromPath('user/show', array(
+ 'backend' => $backend->getName(),
+ 'user' => $userName
+ )),
+ 'decorators' => array('ViewHelper')
+ ));
+ $removeForm->addElement('button', 'btn_submit', array(
+ 'escape' => false,
+ 'type' => 'submit',
+ 'class' => 'link-like',
+ 'value' => 'btn_submit',
+ 'decorators' => array('ViewHelper'),
+ 'label' => $this->view->icon('trash'),
+ 'title' => $this->translate('Cancel this membership')
+ ));
+ $this->view->removeForm = $removeForm;
+ }
+ }
+
+ /**
+ * Add a user
+ */
+ public function addAction()
+ {
+ $this->assertPermission('config/authentication/users/add');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+ $form->add()->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Edit a user
+ */
+ public function editAction()
+ {
+ $this->assertPermission('config/authentication/users/edit');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)));
+ $form->setRepository($backend);
+
+ try {
+ $form->edit($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Remove a user
+ */
+ public function removeAction()
+ {
+ $this->assertPermission('config/authentication/users/remove');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
+
+ $form = new UserForm();
+ $form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
+ $form->setRepository($backend);
+
+ try {
+ $form->remove($userName)->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Create a membership for a user
+ */
+ public function createmembershipAction()
+ {
+ $this->assertPermission('config/authentication/groups/edit');
+ $userName = $this->params->getRequired('user');
+ $backend = $this->getUserBackend($this->params->getRequired('backend'));
+
+ if ($backend->select()->where('user_name', $userName)->count() === 0) {
+ $this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
+ }
+
+ $backends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
+ if (empty($backends)) {
+ throw new ConfigurationError($this->translate(
+ 'You\'ll need to configure at least one user group backend first that allows to create new memberships'
+ ));
+ }
+
+ $form = new CreateMembershipForm();
+ $form->setBackends($backends)
+ ->setUsername($userName)
+ ->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)))
+ ->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Fetch and return the given user's groups from all user group backends
+ *
+ * @param User $user
+ *
+ * @return ArrayDatasource
+ */
+ protected function loadMemberships(User $user)
+ {
+ $groups = $alreadySeen = array();
+ foreach ($this->loadUserGroupBackends() as $backend) {
+ try {
+ foreach ($backend->getMemberships($user) as $groupName) {
+ if (array_key_exists($groupName, $alreadySeen)) {
+ continue; // Ignore duplicate memberships
+ }
+
+ $alreadySeen[$groupName] = null;
+ $groups[] = (object) array(
+ 'group_name' => $groupName,
+ 'backend' => $backend
+ );
+ }
+ } catch (Exception $e) {
+ Logger::error($e);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch memberships from backend %s. Please check your log'),
+ $backend->getName()
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+
+ /**
+ * Create the tabs to display when showing a user
+ *
+ * @param string $backendName
+ * @param string $userName
+ */
+ protected function createShowTabs($backendName, $userName)
+ {
+ $tabs = $this->getTabs();
+ $tabs->add(
+ 'user/show',
+ array(
+ 'title' => sprintf($this->translate('Show user %s'), $userName),
+ 'label' => $this->translate('User'),
+ 'icon' => 'user',
+ 'url' => Url::fromPath('user/show', array('backend' => $backendName, 'user' => $userName))
+ )
+ );
+
+ return $tabs;
+ }
+}
diff --git a/application/controllers/UsergroupbackendController.php b/application/controllers/UsergroupbackendController.php
new file mode 100644
index 000000000..cdb6826be
--- /dev/null
+++ b/application/controllers/UsergroupbackendController.php
@@ -0,0 +1,183 @@
+assertPermission('config/application/usergroupbackend');
+ }
+
+ /**
+ * Redirect to this controller's list action
+ */
+ public function indexAction()
+ {
+ $this->redirectNow('usergroupbackend/list');
+ }
+
+ /**
+ * Show a list of all user group backends
+ */
+ public function listAction()
+ {
+ $this->view->backendNames = Config::app('groups')->keys();
+ $this->createListTabs()->activate('usergroupbackend');
+ }
+
+ /**
+ * Create a new user group backend
+ */
+ public function createAction()
+ {
+ $form = new UserGroupBackendForm();
+ $form->setRedirectUrl('usergroupbackend/list');
+ $form->setTitle($this->translate('Create New User Group Backend'));
+ $form->addDescription($this->translate('Create a new backend to associate users and groups with.'));
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) {
+ try {
+ $form->add($form->getValues());
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(t('User group backend successfully created'));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Edit an user group backend
+ */
+ public function editAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $form = new UserGroupBackendForm();
+ $form->setAction(Url::fromRequest());
+ $form->setRedirectUrl('usergroupbackend/list');
+ $form->setTitle(sprintf($this->translate('Edit User Group Backend %s'), $backendName));
+ $form->setIniConfig(Config::app('groups'));
+ $form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) {
+ try {
+ $form->edit($backendName, $form->getValues());
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($form->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully updated'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+
+ try {
+ $form->load($backendName);
+ $form->handleRequest();
+ } catch (NotFoundError $_) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $backendName));
+ }
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Remove a user group backend
+ */
+ public function removeAction()
+ {
+ $backendName = $this->params->getRequired('backend');
+
+ $backendForm = new UserGroupBackendForm();
+ $backendForm->setIniConfig(Config::app('groups'));
+ $form = new ConfirmRemovalForm();
+ $form->setRedirectUrl('usergroupbackend/list');
+ $form->setTitle(sprintf($this->translate('Remove User Group Backend %s'), $backendName));
+ $form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
+ try {
+ $backendForm->delete($backendName);
+ } catch (Exception $e) {
+ $form->error($e->getMessage());
+ return false;
+ }
+
+ if ($backendForm->save()) {
+ Notification::success(sprintf(t('User group backend "%s" successfully removed'), $backendName));
+ return true;
+ }
+
+ return false;
+ });
+ $form->handleRequest();
+
+ $this->view->form = $form;
+ $this->render('form');
+ }
+
+ /**
+ * Create the tabs for the application configuration
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+ if ($this->hasPermission('config/application/general')) {
+ $tabs->add('general', array(
+ 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
+ 'label' => $this->translate('General'),
+ 'url' => 'config/general'
+ ));
+ }
+ if ($this->hasPermission('config/application/resources')) {
+ $tabs->add('resource', array(
+ 'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'),
+ 'label' => $this->translate('Resources'),
+ 'url' => 'config/resource'
+ ));
+ }
+ if ($this->hasPermission('config/application/userbackend')) {
+ $tabs->add('userbackend', array(
+ 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
+ 'label' => $this->translate('Authentication'),
+ 'url' => 'config/userbackend'
+ ));
+ }
+ if ($this->hasPermission('config/application/usergroupbackend')) {
+ $tabs->add('usergroupbackend', array(
+ 'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'),
+ 'label' => $this->translate('User Groups'),
+ 'url' => 'usergroupbackend/list'
+ ));
+ }
+
+ return $tabs;
+ }
+}
diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php
new file mode 100644
index 000000000..a0b40b4ee
--- /dev/null
+++ b/application/forms/Config/User/CreateMembershipForm.php
@@ -0,0 +1,191 @@
+backends = $backends;
+ return $this;
+ }
+
+ /**
+ * Set the username to create memberships for
+ *
+ * @param string $userName
+ *
+ * @return $this
+ */
+ public function setUsername($userName)
+ {
+ $this->userName = $userName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name'));
+
+ $options = array();
+ foreach ($query as $row) {
+ $options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')';
+ }
+
+ $this->addElement(
+ 'multiselect',
+ 'groups',
+ array(
+ 'required' => true,
+ 'multiOptions' => $options,
+ 'label' => $this->translate('Groups'),
+ 'description' => sprintf(
+ $this->translate('Select one or more groups where to add %s as member'),
+ $this->userName
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+
+ /**
+ * Instantly redirect back in case the user is already a member of all groups
+ */
+ public function onRequest()
+ {
+ if ($this->createDataSource()->select()->from('group')->count() === 0) {
+ Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName));
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ }
+
+ /**
+ * Create the memberships for the user
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $backendMap = array();
+ foreach ($this->backends as $backend) {
+ $backendMap[$backend->getName()] = $backend;
+ }
+
+ $single = null;
+ foreach ($this->getValue('groups') as $backendAndGroup) {
+ list($backendName, $groupName) = explode(';', $backendAndGroup, 2);
+ try {
+ $backendMap[$backendName]->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $groupName,
+ 'user_name' => $this->userName
+ )
+ );
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $this->userName,
+ $groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(
+ sprintf($this->translate('Membership for group %s created successfully'), $groupName)
+ );
+ } else {
+ Notification::success($this->translate('Memberships created successfully'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Create and return a data source to fetch all groups from all backends where the user is not already a member of
+ *
+ * @return ArrayDatasource
+ */
+ protected function createDataSource()
+ {
+ $groups = $failures = array();
+ foreach ($this->backends as $backend) {
+ try {
+ $memberships = $backend
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $this->userName)
+ ->fetchColumn();
+ foreach ($backend->select(array('group_name')) as $row) {
+ if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter
+ $row->backend_name = $backend->getName();
+ $groups[] = $row;
+ }
+ }
+ } catch (Exception $e) {
+ $failures[] = array($backend->getName(), $e);
+ }
+ }
+
+ if (empty($groups) && !empty($failures)) {
+ // In case there are only failures, throw the very first exception again
+ throw $failures[0][1];
+ } elseif (! empty($failures)) {
+ foreach ($failures as $failure) {
+ Logger::error($failure[1]);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any groups from backend %s. Please check your log'),
+ $failure[0]
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+}
diff --git a/application/forms/Config/User/UserForm.php b/application/forms/Config/User/UserForm.php
new file mode 100644
index 000000000..765b95882
--- /dev/null
+++ b/application/forms/Config/User/UserForm.php
@@ -0,0 +1,175 @@
+addElement(
+ 'checkbox',
+ 'is_active',
+ array(
+ 'required' => true,
+ 'value' => true,
+ 'label' => $this->translate('Active'),
+ 'description' => $this->translate('Prevents the user from logging in if unchecked')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'password',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Password')
+ )
+ );
+
+ $this->setTitle($this->translate('Add a new user'));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Create and add elements to this form to update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+
+ $this->addElement(
+ 'text',
+ 'password',
+ array(
+ 'label' => $this->translate('Password')
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * Update a user
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('user', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * Strips off the password if null or the empty string.
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ if (! $values['password']) {
+ unset($values['password']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create and add elements to this form to delete a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a user
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('user_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('User added successfully');
+ } else {
+ return $this->translate('Failed to add user');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier());
+ }
+ }
+}
diff --git a/application/forms/Config/Authentication/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php
similarity index 85%
rename from application/forms/Config/Authentication/DbBackendForm.php
rename to application/forms/Config/UserBackend/DbBackendForm.php
index e96b9637c..f096f3e35 100644
--- a/application/forms/Config/Authentication/DbBackendForm.php
+++ b/application/forms/Config/UserBackend/DbBackendForm.php
@@ -1,16 +1,16 @@
getResourceConfig()));
- if ($dbUserBackend->count() < 1) {
- $form->addError($form->translate('No users found under the specified database backend'));
+ if ($dbUserBackend->select()->where('is_active', true)->count() < 1) {
+ $form->addError($form->translate('No active users found under the specified database backend'));
return false;
}
} catch (Exception $e) {
diff --git a/application/forms/Config/Authentication/ExternalBackendForm.php b/application/forms/Config/UserBackend/ExternalBackendForm.php
similarity index 93%
rename from application/forms/Config/Authentication/ExternalBackendForm.php
rename to application/forms/Config/UserBackend/ExternalBackendForm.php
index 86087eb66..4f3a4585f 100644
--- a/application/forms/Config/Authentication/ExternalBackendForm.php
+++ b/application/forms/Config/UserBackend/ExternalBackendForm.php
@@ -1,13 +1,13 @@
getResourceConfig()),
- $form->getElement('user_class')->getValue(),
- $form->getElement('user_name_attribute')->getValue(),
- $form->getElement('base_dn')->getValue(),
- $form->getElement('filter')->getValue()
- );
+ $ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig()));
+ $ldapUserBackend->setConfig(new ConfigObject($form->getValues()));
$ldapUserBackend->assertAuthenticationPossible();
} catch (AuthenticationException $e) {
if (($previous = $e->getPrevious()) !== null) {
diff --git a/application/forms/Config/AuthenticationBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php
similarity index 87%
rename from application/forms/Config/AuthenticationBackendConfigForm.php
rename to application/forms/Config/UserBackendConfigForm.php
index 86322902b..0a30dd590 100644
--- a/application/forms/Config/AuthenticationBackendConfigForm.php
+++ b/application/forms/Config/UserBackendConfigForm.php
@@ -11,11 +11,11 @@ use Icinga\Application\Platform;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
-use Icinga\Forms\Config\Authentication\DbBackendForm;
-use Icinga\Forms\Config\Authentication\LdapBackendForm;
-use Icinga\Forms\Config\Authentication\ExternalBackendForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
-class AuthenticationBackendConfigForm extends ConfigForm
+class UserBackendConfigForm extends ConfigForm
{
/**
* The available resources split by type
@@ -76,7 +76,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
}
/**
- * Add a particular authentication backend
+ * Add a particular user backend
*
* The backend to add is identified by the array-key `name'.
*
@@ -90,9 +90,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
{
$name = isset($values['name']) ? $values['name'] : '';
if (! $name) {
- throw new InvalidArgumentException($this->translate('Authentication backend name missing'));
+ throw new InvalidArgumentException($this->translate('User backend name missing'));
} elseif ($this->config->hasSection($name)) {
- throw new InvalidArgumentException($this->translate('Authentication backend already exists'));
+ throw new InvalidArgumentException($this->translate('User backend already exists'));
}
unset($values['name']);
@@ -101,7 +101,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
}
/**
- * Edit a particular authentication backend
+ * Edit a particular user backend
*
* @param string $name The name of the backend to edit
* @param array $values The values to edit the configuration with
@@ -113,11 +113,11 @@ class AuthenticationBackendConfigForm extends ConfigForm
public function edit($name, array $values)
{
if (! $name) {
- throw new InvalidArgumentException($this->translate('Old authentication backend name missing'));
+ throw new InvalidArgumentException($this->translate('Old user backend name missing'));
} elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) {
- throw new InvalidArgumentException($this->translate('New authentication backend name missing'));
+ throw new InvalidArgumentException($this->translate('New user backend name missing'));
} elseif (! $this->config->hasSection($name)) {
- throw new InvalidArgumentException($this->translate('Unknown authentication backend provided'));
+ throw new InvalidArgumentException($this->translate('Unknown user backend provided'));
}
$backendConfig = $this->config->getSection($name);
@@ -132,7 +132,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
}
/**
- * Remove the given authentication backend
+ * Remove the given user backend
*
* @param string $name The name of the backend to remove
*
@@ -143,9 +143,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
public function remove($name)
{
if (! $name) {
- throw new InvalidArgumentException($this->translate('Authentication backend name missing'));
+ throw new InvalidArgumentException($this->translate('user backend name missing'));
} elseif (! $this->config->hasSection($name)) {
- throw new InvalidArgumentException($this->translate('Unknown authentication backend provided'));
+ throw new InvalidArgumentException($this->translate('Unknown user backend provided'));
}
$backendConfig = $this->config->getSection($name);
@@ -154,7 +154,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
}
/**
- * Move the given authentication backend up or down in order
+ * Move the given user backend up or down in order
*
* @param string $name The name of the backend to be moved
* @param int $position The new (absolute) position of the backend
@@ -166,9 +166,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
public function move($name, $position)
{
if (! $name) {
- throw new InvalidArgumentException($this->translate('Authentication backend name missing'));
+ throw new InvalidArgumentException($this->translate('User backend name missing'));
} elseif (! $this->config->hasSection($name)) {
- throw new InvalidArgumentException($this->translate('Unknown authentication backend provided'));
+ throw new InvalidArgumentException($this->translate('Unknown user backend provided'));
}
$backendOrder = $this->config->keys();
@@ -186,7 +186,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
}
/**
- * Add or edit an authentication backend and save the configuration
+ * Add or edit an user backend and save the configuration
*
* Performs a connectivity validation using the submitted values. A checkbox is
* added to the form to skip the check if it fails and redirection is aborted.
@@ -197,20 +197,20 @@ class AuthenticationBackendConfigForm extends ConfigForm
{
if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
$backendForm = $this->getBackendForm($this->getElement('type')->getValue());
- if (false === $backendForm::isValidAuthenticationBackend($this)) {
+ if (false === $backendForm::isValidUserBackend($this)) {
$this->addElement($this->getForceCreationCheckbox());
return false;
}
}
- $authBackend = $this->request->getQuery('auth_backend');
+ $authBackend = $this->request->getQuery('backend');
try {
if ($authBackend === null) { // create new backend
$this->add($this->getValues());
- $message = $this->translate('Authentication backend "%s" has been successfully created');
+ $message = $this->translate('User backend "%s" has been successfully created');
} else { // edit existing backend
$this->edit($authBackend, $this->getValues());
- $message = $this->translate('Authentication backend "%s" has been successfully changed');
+ $message = $this->translate('User backend "%s" has been successfully changed');
}
} catch (InvalidArgumentException $e) {
Notification::error($e->getMessage());
@@ -225,7 +225,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
}
/**
- * Populate the form in case an authentication backend is being edited
+ * Populate the form in case an user backend is being edited
*
* @see Form::onRequest()
*
@@ -233,12 +233,12 @@ class AuthenticationBackendConfigForm extends ConfigForm
*/
public function onRequest()
{
- $authBackend = $this->request->getQuery('auth_backend');
+ $authBackend = $this->request->getQuery('backend');
if ($authBackend !== null) {
if ($authBackend === '') {
- throw new ConfigurationError($this->translate('Authentication backend name missing'));
+ throw new ConfigurationError($this->translate('User backend name missing'));
} elseif (! $this->config->hasSection($authBackend)) {
- throw new ConfigurationError($this->translate('Unknown authentication backend provided'));
+ throw new ConfigurationError($this->translate('Unknown user backend provided'));
} elseif ($this->config->getSection($authBackend)->backend === null) {
throw new ConfigurationError(
sprintf($this->translate('Backend "%s" has no `backend\' setting'), $authBackend)
diff --git a/application/forms/Config/AuthenticationBackendReorderForm.php b/application/forms/Config/UserBackendReorderForm.php
similarity index 87%
rename from application/forms/Config/AuthenticationBackendReorderForm.php
rename to application/forms/Config/UserBackendReorderForm.php
index 34f20d851..9069e73e3 100644
--- a/application/forms/Config/AuthenticationBackendReorderForm.php
+++ b/application/forms/Config/UserBackendReorderForm.php
@@ -7,7 +7,7 @@ use InvalidArgumentException;
use Icinga\Web\Notification;
use Icinga\Forms\ConfigForm;
-class AuthenticationBackendReorderForm extends ConfigForm
+class UserBackendReorderForm extends ConfigForm
{
/**
* Initialize this form
@@ -38,7 +38,7 @@ class AuthenticationBackendReorderForm extends ConfigForm
}
/**
- * Update the authentication backend order and save the configuration
+ * Update the user backend order and save the configuration
*
* @see Form::onSuccess()
*/
@@ -62,13 +62,13 @@ class AuthenticationBackendReorderForm extends ConfigForm
}
/**
- * Return the config form for authentication backends
+ * Return the config form for user backends
*
* @return ConfigForm
*/
protected function getConfigForm()
{
- $form = new AuthenticationBackendConfigForm();
+ $form = new UserBackendConfigForm();
$form->setIniConfig($this->config);
return $form;
}
diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php
new file mode 100644
index 000000000..88064dcdd
--- /dev/null
+++ b/application/forms/Config/UserGroup/AddMemberForm.php
@@ -0,0 +1,182 @@
+ds = $ds;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend to use
+ *
+ * @param Extensible $backend
+ *
+ * @return $this
+ */
+ public function setBackend(Extensible $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ /**
+ * Set the group to add members for
+ *
+ * @param string $groupName
+ *
+ * @return $this
+ */
+ public function setGroupName($groupName)
+ {
+ $this->groupName = $groupName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ // TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate
+ // memberships (no matter whether the data source permits it or not, a member does never need to be
+ // added more than once) should be kept at backend level (GroupController::fetchUsers) but this does
+ // not work currently as our ldap protocol stuff is unable to handle our filter implementation..
+ $members = $this->backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $this->groupName)
+ ->fetchColumn();
+ $filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members));
+
+ $users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn();
+ if (! empty($users)) {
+ $this->addElement(
+ 'multiselect',
+ 'user_name',
+ array(
+ 'multiOptions' => array_combine($users, $users),
+ 'label' => $this->translate('Backend Users'),
+ 'description' => $this->translate(
+ 'Select one or more users (fetched from your user backends) to add as group member'
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'required' => empty($users),
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Provide one or more usernames separated by comma to add as group member'
+ )
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Insert the members for the group
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $userNames = $this->getValue('user_name') ?: array();
+ if (($users = $this->getValue('users'))) {
+ $userNames = array_merge($userNames, array_map('trim', explode(',', $users)));
+ }
+
+ if (empty($userNames)) {
+ $this->info($this->translate(
+ 'Please provide at least one username, either by choosing one '
+ . 'in the list or by manually typing one in the text box below'
+ ));
+ return false;
+ }
+
+ $single = null;
+ foreach ($userNames as $userName) {
+ try {
+ $this->backend->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $this->groupName,
+ 'user_name' => $userName
+ )
+ );
+ } catch (NotFoundError $e) {
+ throw $e; // Trigger 404, the group name is initially accessed as GET parameter
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $userName,
+ $this->groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName));
+ } else {
+ Notification::success($this->translate('Group members added successfully'));
+ }
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserGroup/DbUserGroupBackendForm.php b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
new file mode 100644
index 000000000..9f1915968
--- /dev/null
+++ b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
@@ -0,0 +1,58 @@
+setName('form_config_dbusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $resourceNames = $this->getDatabaseResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate('The database connection to use for this backend'),
+ 'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames)
+ )
+ );
+ }
+
+ /**
+ * Return the names of all configured database resources
+ *
+ * @return array
+ */
+ protected function getDatabaseResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (strtolower($config->type) === 'db') {
+ $names[] = $name;
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php
new file mode 100644
index 000000000..dd9875c9a
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php
@@ -0,0 +1,198 @@
+setName('form_config_usergroupbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ */
+ public function getBackendForm($type)
+ {
+ if ($type === 'db') {
+ return new DbUserGroupBackendForm();
+ } else {
+ throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type));
+ }
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ 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);
+ return $this;
+ }
+
+ /**
+ * Add a new user group backend
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName);
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user group backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name']) && $data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ unset($data['name']);
+ }
+
+ $this->config->setSection($name, $backendConfig->merge($data));
+ return $this;
+ }
+
+ /**
+ * Remove a user group backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ ),
+ 'validators' => array(
+ array(
+ 'Regex',
+ false,
+ array(
+ 'pattern' => '/^[^\\[\\]:]+$/',
+ 'messages' => array(
+ 'regexNotMatch' => $this->translate(
+ 'The backend name cannot contain \'[\', \']\' or \':\'.'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ // TODO(jom): We did not think about how to configure custom group backends yet!
+ $backendTypes = array(
+ 'db' => $this->translate('Database')
+ );
+
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => $backendType
+ )
+ );
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate('The type of this user group backend'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ $backendForm = $this->getBackendForm($backendType);
+ $backendForm->createElements($formData);
+ $this->addElements($backendForm->getElements());
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php
new file mode 100644
index 000000000..598029c1d
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupForm.php
@@ -0,0 +1,126 @@
+addElement(
+ 'text',
+ 'group_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name')
+ )
+ );
+
+ if ($this->shouldInsert()) {
+ $this->setTitle($this->translate('Add a new group'));
+ $this->setSubmitLabel($this->translate('Add'));
+ } else { // $this->shouldUpdate()
+ $this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+ }
+
+ /**
+ * Update a group
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('group', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create and add elements to this form to delete a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier()));
+ $this->addDescription($this->translate(
+ 'Note that all users that are currently a member of this group will'
+ . ' have their membership cleared automatically.'
+ ));
+ $this->setSubmitLabel($this->translate('Yes'));
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a group
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('group_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('Group added successfully');
+ } else {
+ return $this->translate('Failed to add group');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier());
+ }
+ }
+}
diff --git a/application/forms/RepositoryForm.php b/application/forms/RepositoryForm.php
new file mode 100644
index 000000000..136855d1e
--- /dev/null
+++ b/application/forms/RepositoryForm.php
@@ -0,0 +1,389 @@
+repository = $repository;
+ return $this;
+ }
+
+ /**
+ * Return the name of the entry to handle
+ *
+ * @return string
+ */
+ protected function getIdentifier()
+ {
+ return $this->identifier;
+ }
+
+ /**
+ * Return the current data of the entry being handled
+ *
+ * @return array
+ */
+ protected function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Return whether an entry should be inserted
+ *
+ * @return bool
+ */
+ public function shouldInsert()
+ {
+ return $this->mode === self::MODE_INSERT;
+ }
+
+ /**
+ * Return whether an entry should be udpated
+ *
+ * @return bool
+ */
+ public function shouldUpdate()
+ {
+ return $this->mode === self::MODE_UPDATE;
+ }
+
+ /**
+ * Return whether an entry should be deleted
+ *
+ * @return bool
+ */
+ public function shouldDelete()
+ {
+ return $this->mode === self::MODE_DELETE;
+ }
+
+ /**
+ * Add a new entry
+ *
+ * @param array $data The defaults to use, if any
+ *
+ * @return $this
+ */
+ public function add(array $data = array())
+ {
+ $this->mode = static::MODE_INSERT;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Edit an entry
+ *
+ * @param string $name The entry's name
+ * @param array $data The entry's current data
+ *
+ * @return $this
+ */
+ public function edit($name, array $data = array())
+ {
+ $this->mode = static::MODE_UPDATE;
+ $this->identifier = $name;
+ $this->data = $data;
+ return $this;
+ }
+
+ /**
+ * Remove an entry
+ *
+ * @param string $name The entry's name
+ *
+ * @return $this
+ */
+ public function remove($name)
+ {
+ $this->mode = static::MODE_DELETE;
+ $this->identifier = $name;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ if ($this->shouldInsert()) {
+ $this->createInsertElements($formData);
+ } elseif ($this->shouldUpdate()) {
+ $this->createUpdateElements($formData);
+ } elseif ($this->shouldDelete()) {
+ $this->createDeleteElements($formData);
+ }
+ }
+
+ /**
+ * Prepare the form for the requested mode
+ */
+ public function onRequest()
+ {
+ if ($this->shouldInsert()) {
+ $this->onInsertRequest();
+ } elseif ($this->shouldUpdate()) {
+ $this->onUpdateRequest();
+ } elseif ($this->shouldDelete()) {
+ $this->onDeleteRequest();
+ }
+ }
+
+ /**
+ * Prepare the form for mode insert
+ *
+ * Populates the form with the data passed to add().
+ */
+ protected function onInsertRequest()
+ {
+ $data = $this->getData();
+ if (! empty($data)) {
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Prepare the form for mode update
+ *
+ * Populates the form with either the data passed to edit() or tries to fetch it from the repository.
+ *
+ * @throws NotFoundError In case the entry to update cannot be found
+ */
+ protected function onUpdateRequest()
+ {
+ $data = $this->getData();
+ if (empty($data)) {
+ $row = $this->repository->select()->applyFilter($this->createFilter())->fetchRow();
+ if ($row === false) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+
+ $data = get_object_vars($row);
+ }
+
+ $this->populate($data);
+ }
+
+ /**
+ * Prepare the form for mode delete
+ *
+ * Verifies that the repository contains the entry to delete.
+ *
+ * @throws NotFoundError In case the entry to delete cannot be found
+ */
+ protected function onDeleteRequest()
+ {
+ if ($this->repository->select()->addFilter($this->createFilter())->count() === 0) {
+ throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Apply the requested mode on the repository
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ if ($this->shouldInsert()) {
+ return $this->onInsertSuccess();
+ } elseif ($this->shouldUpdate()) {
+ return $this->onUpdateSuccess();
+ } elseif ($this->shouldDelete()) {
+ return $this->onDeleteSuccess();
+ }
+ }
+
+ /**
+ * Apply mode insert on the repository
+ *
+ * @return bool
+ */
+ protected function onInsertSuccess()
+ {
+ try {
+ $this->repository->insert(
+ $this->repository->getBaseTable(),
+ $this->getValues()
+ );
+ } catch (Exception $e) {
+ Notification::error($this->getInsertMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getInsertMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode update on the repository
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ try {
+ $this->repository->update(
+ $this->repository->getBaseTable(),
+ $this->getValues(),
+ $this->createFilter()
+ );
+ } catch (Exception $e) {
+ Notification::error($this->getUpdateMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getUpdateMessage(true));
+ return true;
+ }
+
+ /**
+ * Apply mode delete on the repository
+ *
+ * @return bool
+ */
+ protected function onDeleteSuccess()
+ {
+ try {
+ $this->repository->delete(
+ $this->repository->getBaseTable(),
+ $this->createFilter()
+ );
+ } catch (Exception $e) {
+ Notification::error($this->getDeleteMessage(false));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ Notification::success($this->getDeleteMessage(true));
+ return true;
+ }
+
+ /**
+ * Create and add elements to this form to insert an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createInsertElements(array $formData);
+
+ /**
+ * Create and add elements to this form to update an entry
+ *
+ * Calls createInsertElements() by default. Overwrite this to add different elements when in mode update.
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+ }
+
+ /**
+ * Create and add elements to this form to delete an entry
+ *
+ * @param array $formData The data sent by the user
+ */
+ abstract protected function createDeleteElements(array $formData);
+
+ /**
+ * Create and return a filter to use when selecting, updating or deleting an entry
+ *
+ * @return Filter
+ */
+ abstract protected function createFilter();
+
+ /**
+ * Return a notification message to use when inserting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getInsertMessage($success);
+
+ /**
+ * Return a notification message to use when updating an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getUpdateMessage($success);
+
+ /**
+ * Return a notification message to use when deleting an entry
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ abstract protected function getDeleteMessage($success);
+}
diff --git a/application/forms/Security/RoleForm.php b/application/forms/Security/RoleForm.php
index 07b09ce17..ee5d312f0 100644
--- a/application/forms/Security/RoleForm.php
+++ b/application/forms/Security/RoleForm.php
@@ -21,14 +21,30 @@ class RoleForm extends ConfigForm
* @var array
*/
protected $providedPermissions = array(
- '*' => '*',
- 'config/*' => 'config/*',
- 'config/application/*' => 'config/application/*',
- 'config/application/general' => 'config/application/general',
- 'config/application/authentication' => 'config/application/authentication',
- 'config/application/resources' => 'config/application/resources',
- 'config/application/roles' => 'config/application/roles',
- 'config/modules' => 'config/modules'
+ '*' => '*',
+ 'config/*' => 'config/*',
+ 'config/application/*' => 'config/application/*',
+ 'config/application/general' => 'config/application/general',
+ 'config/application/resources' => 'config/application/resources',
+ 'config/application/userbackend' => 'config/application/userbackend',
+ 'config/application/usergroupbackend' => 'config/application/usergroupbackend',
+ 'config/authentication/*' => 'config/authentication/*',
+ 'config/authentication/users/*' => 'config/authentication/users/*',
+ 'config/authentication/users/show' => 'config/authentication/users/show',
+ 'config/authentication/users/add' => 'config/authentication/users/add',
+ 'config/authentication/users/edit' => 'config/authentication/users/edit',
+ 'config/authentication/users/remove' => 'config/authentication/users/remove',
+ 'config/authentication/groups/*' => 'config/authentication/groups/*',
+ 'config/authentication/groups/show' => 'config/authentication/groups/show',
+ 'config/authentication/groups/add' => 'config/authentication/groups/add',
+ 'config/authentication/groups/edit' => 'config/authentication/groups/edit',
+ 'config/authentication/groups/remove' => 'config/authentication/groups/remove',
+ 'config/authentication/roles/*' => 'config/authentication/roles/*',
+ 'config/authentication/roles/show' => 'config/authentication/roles/show',
+ 'config/authentication/roles/add' => 'config/authentication/roles/add',
+ 'config/authentication/roles/edit' => 'config/authentication/roles/edit',
+ 'config/authentication/roles/remove' => 'config/authentication/roles/remove',
+ 'config/modules' => 'config/modules'
);
/**
diff --git a/application/views/scripts/config/application.phtml b/application/views/scripts/config/general.phtml
similarity index 100%
rename from application/views/scripts/config/application.phtml
rename to application/views/scripts/config/general.phtml
diff --git a/application/views/scripts/config/authentication/create.phtml b/application/views/scripts/config/userbackend/create.phtml
similarity index 100%
rename from application/views/scripts/config/authentication/create.phtml
rename to application/views/scripts/config/userbackend/create.phtml
diff --git a/application/views/scripts/config/authentication/modify.phtml b/application/views/scripts/config/userbackend/modify.phtml
similarity index 100%
rename from application/views/scripts/config/authentication/modify.phtml
rename to application/views/scripts/config/userbackend/modify.phtml
diff --git a/application/views/scripts/config/authentication/remove.phtml b/application/views/scripts/config/userbackend/remove.phtml
similarity index 100%
rename from application/views/scripts/config/authentication/remove.phtml
rename to application/views/scripts/config/userbackend/remove.phtml
diff --git a/application/views/scripts/config/authentication/reorder.phtml b/application/views/scripts/config/userbackend/reorder.phtml
similarity index 71%
rename from application/views/scripts/config/authentication/reorder.phtml
rename to application/views/scripts/config/userbackend/reorder.phtml
index e4b72d7e1..64a6fc594 100644
--- a/application/views/scripts/config/authentication/reorder.phtml
+++ b/application/views/scripts/config/userbackend/reorder.phtml
@@ -2,8 +2,8 @@
= $tabs; ?>
-
- = $this->icon('plus'); ?>= $this->translate('Create A New Authentication Backend'); ?>
+
+ = $this->icon('plus'); ?>= $this->translate('Create A New User Backend'); ?>
= $form; ?>
diff --git a/application/views/scripts/form/reorder-authbackend.phtml b/application/views/scripts/form/reorder-authbackend.phtml
index cd8001436..20d4e3696 100644
--- a/application/views/scripts/form/reorder-authbackend.phtml
+++ b/application/views/scripts/form/reorder-authbackend.phtml
@@ -12,22 +12,22 @@
= $this->qlink(
$backendNames[$i],
- 'config/editAuthenticationBackend',
- array('auth_backend' => $backendNames[$i]),
+ 'config/edituserbackend',
+ array('backend' => $backendNames[$i]),
array(
'icon' => 'edit',
- 'title' => sprintf($this->translate('Edit authentication backend %s'), $backendNames[$i])
+ 'title' => sprintf($this->translate('Edit user backend %s'), $backendNames[$i])
)
); ?>
|
= $this->qlink(
'',
- 'config/removeAuthenticationBackend',
- array('auth_backend' => $backendNames[$i]),
+ 'config/removeuserbackend',
+ array('backend' => $backendNames[$i]),
array(
'icon' => 'trash',
- 'title' => sprintf($this->translate('Remove authentication backend %s'), $backendNames[$i])
+ 'title' => sprintf($this->translate('Remove user backend %s'), $backendNames[$i])
)
); ?>
|
@@ -40,7 +40,7 @@
); ?>" title="= $this->translate(
'Move up in authentication order'
); ?>" aria-label="= sprintf(
- $this->translate('Move authentication backend %s upwards'),
+ $this->translate('Move user backend %s upwards'),
$backendNames[$i]
); ?>">
= $this->icon('up-big'); ?>
@@ -54,7 +54,7 @@
); ?>" title="= $this->translate(
'Move down in authentication order'
); ?>" aria-label="= sprintf(
- $this->translate('Move authentication backend %s downwards'),
+ $this->translate('Move user backend %s downwards'),
$backendNames[$i]
); ?>">
= $this->icon('down-big'); ?>
diff --git a/application/views/scripts/roles/new.phtml b/application/views/scripts/group/form.phtml
similarity index 50%
rename from application/views/scripts/roles/new.phtml
rename to application/views/scripts/group/form.phtml
index ca1e1559e..cbf06590d 100644
--- a/application/views/scripts/roles/new.phtml
+++ b/application/views/scripts/group/form.phtml
@@ -1,6 +1,6 @@
- = $tabs->showOnlyCloseButton() ?>
+ = $tabs->showOnlyCloseButton(); ?>
- = $form ?>
+ = $form; ?>
\ No newline at end of file
diff --git a/application/views/scripts/group/list.phtml b/application/views/scripts/group/list.phtml
new file mode 100644
index 000000000..bcd9dca93
--- /dev/null
+++ b/application/views/scripts/group/list.phtml
@@ -0,0 +1,78 @@
+compact): ?>
+
+ = $this->tabs; ?>
+ = $this->sortBox; ?>
+ = $this->limiter; ?>
+ = $this->paginator; ?>
+
+ = $this->backendSelection; ?>
+ = $this->filterEditor; ?>
+
+
+
+
+translate('No backend found which is able to list groups') . '
';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/authentication/groups/remove') && $backend instanceof Reducible;
+}
+
+if (count($groups) > 0): ?>
+
+
+
+ = $this->translate('Group'); ?> |
+
+ = $this->translate('Remove'); ?> |
+
+
+
+
+
+
+ = $this->qlink($group->group_name, 'group/show', array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ), array(
+ 'title' => sprintf($this->translate('Show detailed information for group %s'), $group->group_name)
+ )); ?> |
+
+
+ = $this->qlink(
+ null,
+ 'group/remove',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Remove group %s'), $group->group_name),
+ 'icon' => 'trash'
+ )
+ ); ?>
+ |
+
+
+
+
+
+
+
= $this->translate('No groups found matching the filter'); ?>
+
+
+= $this->qlink($this->translate('Add a new group'), 'group/add', array('backend' => $backend->getName()), array(
+ 'icon' => 'plus',
+ 'data-base-target' => '_next',
+ 'class' => 'group-add'
+)); ?>
+
+
\ No newline at end of file
diff --git a/application/views/scripts/group/show.phtml b/application/views/scripts/group/show.phtml
new file mode 100644
index 000000000..636a449d4
--- /dev/null
+++ b/application/views/scripts/group/show.phtml
@@ -0,0 +1,81 @@
+hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
+
+$editLink = null;
+if ($this->hasPermission('config/authentication/groups/edit') && $backend instanceof Updatable) {
+ $editLink = $this->qlink(
+ null,
+ 'group/edit',
+ array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit group %s'), $group->group_name),
+ 'class' => 'group-edit',
+ 'icon' => 'edit'
+ )
+ );
+}
+
+?>
+
+ compact): ?>
+ = $tabs; ?>
+
+
+ compact): ?>
+ = $this->sortBox; ?>
+
+ = $this->limiter; ?>
+ = $this->paginator; ?>
+ compact): ?>
+ = $this->filterEditor; ?>
+
+
+
+ 0): ?>
+
+
+
+ = $this->translate('Username'); ?> |
+
+ = $this->translate('Remove'); ?> |
+
+
+
+
+
+
+ = $this->escape($member->user_name); ?> |
+
+
+ getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
+ |
+
+
+
+
+
+
+
= $this->translate('No group member found matching the filter'); ?>
+
+
+ = $this->qlink($this->translate('Add a new member'), 'group/addmember', array(
+ 'backend' => $backend->getName(),
+ 'group' => $group->group_name
+ ), array(
+ 'icon' => 'plus',
+ 'data-base-target' => '_next',
+ 'class' => 'member-add'
+ )); ?>
+
+
\ No newline at end of file
diff --git a/application/views/scripts/roles/remove.phtml b/application/views/scripts/role/form.phtml
similarity index 50%
rename from application/views/scripts/roles/remove.phtml
rename to application/views/scripts/role/form.phtml
index ca1e1559e..cbf06590d 100644
--- a/application/views/scripts/roles/remove.phtml
+++ b/application/views/scripts/role/form.phtml
@@ -1,6 +1,6 @@
- = $tabs->showOnlyCloseButton() ?>
+ = $tabs->showOnlyCloseButton(); ?>
- = $form ?>
+ = $form; ?>
\ No newline at end of file
diff --git a/application/views/scripts/roles/index.phtml b/application/views/scripts/role/list.phtml
similarity index 95%
rename from application/views/scripts/roles/index.phtml
rename to application/views/scripts/role/list.phtml
index 17e947249..766ba26f3 100644
--- a/application/views/scripts/roles/index.phtml
+++ b/application/views/scripts/role/list.phtml
@@ -22,7 +22,7 @@
= $this->qlink(
$name,
- 'roles/update',
+ 'role/edit',
array('role' => $name),
array('title' => sprintf($this->translate('Edit role %s'), $name))
); ?>
@@ -54,7 +54,7 @@
|
= $this->qlink(
'',
- 'roles/remove',
+ 'role/remove',
array('role' => $name),
array(
'icon' => 'trash',
@@ -67,7 +67,7 @@
-
+
= $this->translate('Create a New Role') ?>
diff --git a/application/views/scripts/roles/update.phtml b/application/views/scripts/user/form.phtml
similarity index 50%
rename from application/views/scripts/roles/update.phtml
rename to application/views/scripts/user/form.phtml
index ca1e1559e..cbf06590d 100644
--- a/application/views/scripts/roles/update.phtml
+++ b/application/views/scripts/user/form.phtml
@@ -1,6 +1,6 @@
- = $tabs->showOnlyCloseButton() ?>
+ = $tabs->showOnlyCloseButton(); ?>
- = $form ?>
+ = $form; ?>
\ No newline at end of file
diff --git a/application/views/scripts/user/list.phtml b/application/views/scripts/user/list.phtml
new file mode 100644
index 000000000..76a6f2b8b
--- /dev/null
+++ b/application/views/scripts/user/list.phtml
@@ -0,0 +1,78 @@
+compact): ?>
+
+ = $this->tabs; ?>
+ = $this->sortBox; ?>
+ = $this->limiter; ?>
+ = $this->paginator; ?>
+
+ = $this->backendSelection; ?>
+ = $this->filterEditor; ?>
+
+
+
+
+translate('No backend found which is able to list users') . ' ';
+ return;
+} else {
+ $extensible = $this->hasPermission('config/authentication/users/add') && $backend instanceof Extensible;
+ $reducible = $this->hasPermission('config/authentication/users/remove') && $backend instanceof Reducible;
+}
+
+if (count($users) > 0): ?>
+
+
+
+ = $this->translate('Username'); ?> |
+
+ = $this->translate('Remove'); ?> |
+
+
+
+
+
+
+ = $this->qlink($user->user_name, 'user/show', array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ), array(
+ 'title' => sprintf($this->translate('Show detailed information about %s'), $user->user_name)
+ )); ?> |
+
+
+ = $this->qlink(
+ null,
+ 'user/remove',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Remove user %s'), $user->user_name),
+ 'icon' => 'trash'
+ )
+ ); ?>
+ |
+
+
+
+
+
+
+= $this->translate('No users found matching the filter'); ?>
+
+
+= $this->qlink($this->translate('Add a new user'), 'user/add', array('backend' => $backend->getName()), array(
+ 'icon' => 'plus',
+ 'data-base-target' => '_next',
+ 'class' => 'user-add'
+)); ?>
+
+
\ No newline at end of file
diff --git a/application/views/scripts/user/show.phtml b/application/views/scripts/user/show.phtml
new file mode 100644
index 000000000..82f4c53f9
--- /dev/null
+++ b/application/views/scripts/user/show.phtml
@@ -0,0 +1,95 @@
+hasPermission('config/authentication/users/edit') && $backend instanceof Updatable) {
+ $editLink = $this->qlink(
+ null,
+ 'user/edit',
+ array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+ ),
+ array(
+ 'title' => sprintf($this->translate('Edit user %s'), $user->user_name),
+ 'class' => 'user-edit',
+ 'icon' => 'edit'
+ )
+ );
+}
+
+?>
+
+ compact): ?>
+ = $tabs; ?>
+
+
+ compact): ?>
+ = $this->sortBox; ?>
+
+ = $this->limiter; ?>
+ = $this->paginator; ?>
+ compact): ?>
+ = $this->filterEditor; ?>
+
+
+
+ 0): ?>
+
+
+
+ = $this->translate('Group'); ?> |
+ = $this->translate('Cancel', 'group.membership'); ?> |
+
+
+
+
+
+
+ hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?>
+ = $this->qlink($membership->group_name, 'group/show', array(
+ 'backend' => $membership->backend->getName(),
+ 'group' => $membership->group_name
+ ), array(
+ 'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name)
+ )); ?>
+
+ = $this->escape($membership->group_name); ?>
+
+ |
+
+ backend instanceof Reducible): ?>
+ = $removeForm->setAction($this->url('group/removemember', array(
+ 'backend' => $membership->backend->getName(),
+ 'group' => $membership->group_name
+ ))); ?>
+
+ -
+
+ |
+
+
+
+
+
+ = $this->translate('No memberships found matching the filter'); ?>
+
+
+= $this->qlink($this->translate('Create new membership'), 'user/createmembership', array(
+ 'backend' => $backend->getName(),
+ 'user' => $user->user_name
+), array(
+ 'icon' => 'plus',
+ 'data-base-target' => '_next',
+ 'class' => 'membership-create'
+)); ?>
+
+
\ No newline at end of file
diff --git a/application/views/scripts/usergroupbackend/form.phtml b/application/views/scripts/usergroupbackend/form.phtml
new file mode 100644
index 000000000..cbf06590d
--- /dev/null
+++ b/application/views/scripts/usergroupbackend/form.phtml
@@ -0,0 +1,6 @@
+
+ = $tabs->showOnlyCloseButton(); ?>
+
+
+ = $form; ?>
+
\ No newline at end of file
diff --git a/application/views/scripts/usergroupbackend/list.phtml b/application/views/scripts/usergroupbackend/list.phtml
new file mode 100644
index 000000000..58aa2deba
--- /dev/null
+++ b/application/views/scripts/usergroupbackend/list.phtml
@@ -0,0 +1,46 @@
+
+ = $tabs; ?>
+
+
+= $this->qlink(
+ $this->translate('Create A New User Group Backend'),
+ 'usergroupbackend/create',
+ null,
+ array(
+ 'icon' => 'plus'
+ )
+); ?>
+ 0): ?>
+
+
+
+ = $this->translate('Backend'); ?> |
+ = $this->translate('Remove'); ?> |
+
+
+
+
+
+
+ = $this->qlink(
+ $backendName,
+ 'usergroupbackend/edit',
+ array('backend' => $backendName),
+ array('title' => sprintf($this->translate('Edit user group backend %s'), $backendName))
+ ); ?>
+ |
+ = $this->qlink(
+ null,
+ 'usergroupbackend/remove',
+ array('backend' => $backendName),
+ array(
+ 'title' => sprintf($this->translate('Remove user group backend %s'), $backendName),
+ 'icon' => 'trash'
+ )
+ ); ?> |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/etc/schema/mysql.schema.sql b/etc/schema/mysql.schema.sql
index 16ba9f4ff..5f22aead3 100644
--- a/etc/schema/mysql.schema.sql
+++ b/etc/schema/mysql.schema.sql
@@ -1,21 +1,25 @@
# Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+
CREATE TABLE `icingaweb_group`(
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
- `parent` varchar(64) COLLATE utf8_unicode_ci NULL DEFAULT NULL,
+ `parent` int(10) unsigned NULL DEFAULT NULL,
`ctime` timestamp NULL DEFAULT NULL,
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (`name`)
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_name` (`name`),
+ CONSTRAINT `fk_icingaweb_group_parent_id` FOREIGN KEY (`parent`)
+ REFERENCES `icingaweb_group` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `icingaweb_group_membership`(
- `group_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
+ `group_id` int(10) unsigned NOT NULL,
`username` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`ctime` timestamp NULL DEFAULT NULL,
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (`group_name`,`username`),
- CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_name`)
- REFERENCES `icingaweb_group` (`name`)
+ PRIMARY KEY (`group_id`,`username`),
+ CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_id`)
+ REFERENCES `icingaweb_group` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `icingaweb_user`(
diff --git a/etc/schema/pgsql.schema.sql b/etc/schema/pgsql.schema.sql
index 034d8288a..56117d4f8 100644
--- a/etc/schema/pgsql.schema.sql
+++ b/etc/schema/pgsql.schema.sql
@@ -1,8 +1,13 @@
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
+CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
+ SELECT EXTRACT(EPOCH FROM $1)::bigint AS result
+' LANGUAGE sql;
+
CREATE TABLE "icingaweb_group" (
+ "id" serial,
"name" character varying(64) NOT NULL,
- "parent" character varying(64) NULL DEFAULT NULL,
+ "parent" int NULL DEFAULT NULL,
"ctime" timestamp NULL DEFAULT NULL,
"mtime" timestamp NULL DEFAULT NULL
);
@@ -10,7 +15,7 @@ CREATE TABLE "icingaweb_group" (
ALTER TABLE ONLY "icingaweb_group"
ADD CONSTRAINT pk_icingaweb_group
PRIMARY KEY (
- "name"
+ "id"
);
CREATE UNIQUE INDEX idx_icingaweb_group
@@ -19,8 +24,17 @@ CREATE UNIQUE INDEX idx_icingaweb_group
lower((name)::text)
);
+ALTER TABLE ONLY "icingaweb_group"
+ ADD CONSTRAINT fk_icingaweb_group_parent_id
+ FOREIGN KEY (
+ "parent"
+ )
+ REFERENCES "icingaweb_group" (
+ "id"
+);
+
CREATE TABLE "icingaweb_group_membership" (
- "group_name" character varying(64) NOT NULL,
+ "group_id" int NOT NULL,
"username" character varying(64) NOT NULL,
"ctime" timestamp NULL DEFAULT NULL,
"mtime" timestamp NULL DEFAULT NULL
@@ -28,15 +42,17 @@ CREATE TABLE "icingaweb_group_membership" (
ALTER TABLE ONLY "icingaweb_group_membership"
ADD CONSTRAINT pk_icingaweb_group_membership
- PRIMARY KEY (
- "group_name",
- "username"
+ FOREIGN KEY (
+ "group_id"
+ )
+ REFERENCES "icingaweb_group" (
+ "id"
);
CREATE UNIQUE INDEX idx_icingaweb_group_membership
ON "icingaweb_group_membership"
USING btree (
- lower((group_name)::text),
+ group_id,
lower((username)::text)
);
diff --git a/library/Icinga/Application/Config.php b/library/Icinga/Application/Config.php
index 8a1746b69..02851b975 100644
--- a/library/Icinga/Application/Config.php
+++ b/library/Icinga/Application/Config.php
@@ -9,13 +9,15 @@ use LogicException;
use UnexpectedValueException;
use Icinga\Util\File;
use Icinga\Data\ConfigObject;
+use Icinga\Data\Selectable;
+use Icinga\Data\SimpleQuery;
use Icinga\File\Ini\IniWriter;
use Icinga\Exception\NotReadableError;
/**
* Container for INI like configuration and global registry of application and module related configuration.
*/
-class Config implements Countable, Iterator
+class Config implements Countable, Iterator, Selectable
{
/**
* Configuration directory where ALL (application and module) configuration is located
@@ -85,6 +87,26 @@ class Config implements Countable, Iterator
return $this;
}
+ /**
+ * Return the internal ConfigObject
+ *
+ * @return ConfigObject
+ */
+ public function getConfigObject()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Provide a query for the internal config object
+ *
+ * @return SimpleQuery
+ */
+ public function select()
+ {
+ return $this->config->select();
+ }
+
/**
* Return the count of available sections
*
@@ -92,7 +114,7 @@ class Config implements Countable, Iterator
*/
public function count()
{
- return $this->config->count();
+ return $this->select()->count();
}
/**
diff --git a/library/Icinga/Application/Logger.php b/library/Icinga/Application/Logger.php
index 925695dad..26241b3d5 100644
--- a/library/Icinga/Application/Logger.php
+++ b/library/Icinga/Application/Logger.php
@@ -243,7 +243,9 @@ class Logger
return vsprintf(
array_shift($arguments),
array_map(
- function ($a) { return is_string($a) ? $a : json_encode($a); },
+ function ($a) {
+ return is_string($a) ? $a : ($a instanceof Exception ? $a->getMessage() : json_encode($a));
+ },
$arguments
)
);
diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php
index e8481aab8..54e23d02c 100644
--- a/library/Icinga/Application/Modules/Module.php
+++ b/library/Icinga/Application/Modules/Module.php
@@ -179,10 +179,26 @@ class Module
protected $paneItems = array();
/**
+ * A set of objects representing a searchUrl configuration
+ *
* @var array
*/
protected $searchUrls = array();
+ /**
+ * This module's user backends providing several authentication mechanisms
+ *
+ * @var array
+ */
+ protected $userBackends = array();
+
+ /**
+ * This module's user group backends
+ *
+ * @var array
+ */
+ protected $userGroupBackends = array();
+
/**
* Provide a search URL
*
@@ -201,6 +217,11 @@ class Module
$this->searchUrls[] = $searchUrl;
}
+ /**
+ * Return this module's search urls
+ *
+ * @return array
+ */
public function getSearchUrls()
{
$this->launchConfigScript();
@@ -702,6 +723,28 @@ class Module
return new $this->setupWizard;
}
+ /**
+ * Return this module's user backends
+ *
+ * @return array
+ */
+ public function getUserBackends()
+ {
+ $this->launchConfigScript();
+ return $this->userBackends;
+ }
+
+ /**
+ * Return this module's user group backends
+ *
+ * @return array
+ */
+ public function getUserGroupBackends()
+ {
+ $this->launchConfigScript();
+ return $this->userGroupBackends;
+ }
+
/**
* Provide a named permission
*
@@ -777,6 +820,34 @@ class Module
return $this;
}
+ /**
+ * Provide a user backend capable of authenticating users
+ *
+ * @param string $identifier The identifier of the new backend type
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideUserBackend($identifier, $className)
+ {
+ $this->userBackends[strtolower($identifier)] = $className;
+ return $this;
+ }
+
+ /**
+ * Provide a user group backend
+ *
+ * @param string $identifier The identifier of the new backend type
+ * @param string $className The name of the class
+ *
+ * @return $this
+ */
+ protected function provideUserGroupBackend($identifier, $className)
+ {
+ $this->userGroupBackends[strtolower($identifier)] = $className;
+ return $this;
+ }
+
/**
* Register new namespaces on the autoloader
*
diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php
index 9b9f873c5..7354825a2 100644
--- a/library/Icinga/Authentication/AuthChain.php
+++ b/library/Icinga/Authentication/AuthChain.php
@@ -5,6 +5,8 @@ namespace Icinga\Authentication;
use Iterator;
use Icinga\Data\ConfigObject;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\User\UserBackendInterface;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError;
@@ -24,7 +26,7 @@ class AuthChain implements Iterator
/**
* The consecutive user backend while looping
*
- * @var UserBackend
+ * @var UserBackendInterface
*/
private $currentBackend;
@@ -52,7 +54,7 @@ class AuthChain implements Iterator
/**
* Return the current user backend
*
- * @return UserBackend
+ * @return UserBackendInterface
*/
public function current()
{
diff --git a/library/Icinga/Authentication/Backend/DbUserBackend.php b/library/Icinga/Authentication/Backend/DbUserBackend.php
deleted file mode 100644
index 12fa0392f..000000000
--- a/library/Icinga/Authentication/Backend/DbUserBackend.php
+++ /dev/null
@@ -1,206 +0,0 @@
-conn = $conn;
- }
-
- /**
- * Test whether the given user exists
- *
- * @param User $user
- *
- * @return bool
- */
- public function hasUser(User $user)
- {
- $select = new Zend_Db_Select($this->conn->getDbAdapter());
- $row = $select->from('icingaweb_user', array(new Zend_Db_Expr(1)))
- ->where('name = ?', $user->getUsername())
- ->query()->fetchObject();
-
- return ($row !== false) ? true : false;
- }
-
- /**
- * Add a new user
- *
- * @param string $username The name of the new user
- * @param string $password The new user's password
- * @param bool $active Whether the user is active
- */
- public function addUser($username, $password, $active = true)
- {
- $passwordHash = $this->hashPassword($password);
-
- $stmt = $this->conn->getDbAdapter()->prepare(
- 'INSERT INTO icingaweb_user VALUES (:name, :active, :password_hash, now(), DEFAULT);'
- );
- $stmt->bindParam(':name', $username, PDO::PARAM_STR);
- $stmt->bindParam(':active', $active, PDO::PARAM_INT);
- $stmt->bindParam(':password_hash', $passwordHash, PDO::PARAM_LOB);
- $stmt->execute();
- }
-
- /**
- * Fetch the hashed password for the given user
- *
- * @param string $username The name of the user
- *
- * @return string
- */
- protected function getPasswordHash($username)
- {
- if ($this->conn->getDbType() === 'pgsql') {
- // Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
- $stmt = $this->conn->getDbAdapter()->prepare(
- 'SELECT ENCODE(password_hash, \'escape\') FROM icingaweb_user WHERE name = :name AND active = 1'
- );
- } else {
- $stmt = $this->conn->getDbAdapter()->prepare(
- 'SELECT password_hash FROM icingaweb_user WHERE name = :name AND active = 1'
- );
- }
-
- $stmt->execute(array(':name' => $username));
- $stmt->bindColumn(1, $lob, PDO::PARAM_LOB);
- $stmt->fetch(PDO::FETCH_BOUND);
- if (is_resource($lob)) {
- $lob = stream_get_contents($lob);
- }
-
- return $this->conn->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob;
- }
-
- /**
- * Authenticate the given user and return true on success, false on failure and throw an exception on error
- *
- * @param User $user
- * @param string $password
- *
- * @return bool
- *
- * @throws AuthenticationException
- */
- public function authenticate(User $user, $password)
- {
- try {
- $passwordHash = $this->getPasswordHash($user->getUsername());
- $passwordSalt = $this->getSalt($passwordHash);
- $hashToCompare = $this->hashPassword($password, $passwordSalt);
- return $hashToCompare === $passwordHash;
- } catch (Exception $e) {
- throw new AuthenticationException(
- 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
- $user->getUsername(),
- $this->getName(),
- $e
- );
- }
- }
-
- /**
- * Extract salt from the given password hash
- *
- * @param string $hash The hashed password
- *
- * @return string
- */
- protected function getSalt($hash)
- {
- return substr($hash, strlen(self::HASH_ALGORITHM), self::SALT_LENGTH);
- }
-
- /**
- * Return a random salt
- *
- * The returned salt is safe to be used for hashing a user's password
- *
- * @return string
- */
- protected function generateSalt()
- {
- return openssl_random_pseudo_bytes(self::SALT_LENGTH);
- }
-
- /**
- * Hash a password
- *
- * @param string $password
- * @param string $salt
- *
- * @return string
- */
- protected function hashPassword($password, $salt = null)
- {
- return crypt($password, self::HASH_ALGORITHM . ($salt !== null ? $salt : $this->generateSalt()));
- }
-
- /**
- * Get the number of users available
- *
- * @return int
- */
- public function count()
- {
- $select = new Zend_Db_Select($this->conn->getDbAdapter());
- $row = $select->from(
- 'icingaweb_user',
- array('count' => 'COUNT(*)')
- )->query()->fetchObject();
-
- return ($row !== false) ? $row->count : 0;
- }
-
- /**
- * Return the names of all available users
- *
- * @return array
- */
- public function listUsers()
- {
- $query = $this->conn->select()->from('icingaweb_user', array('name'));
-
- $users = array();
- foreach ($query->fetchAll() as $row) {
- $users[] = $row->name;
- }
-
- return $users;
- }
-}
diff --git a/library/Icinga/Authentication/Backend/DbUserGroupBackend.php b/library/Icinga/Authentication/Backend/DbUserGroupBackend.php
deleted file mode 100644
index d2230bf04..000000000
--- a/library/Icinga/Authentication/Backend/DbUserGroupBackend.php
+++ /dev/null
@@ -1,62 +0,0 @@
-conn = $conn;
- }
-
- /**
- * (non-PHPDoc)
- * @see UserGroupBackend::getMemberships() For the method documentation.
- */
- public function getMemberships(User $user)
- {
- $groups = array();
- $groupsStmt = $this->conn->getDbAdapter()
- ->select()
- ->from($this->conn->getTablePrefix() . 'group', array('name', 'parent'))
- ->query();
- foreach ($groupsStmt as $group) {
- $groups[$group->name] = $group->parent;
- }
- $memberships = array();
- $membershipsStmt = $this->conn->getDbAdapter()
- ->select()
- ->from($this->conn->getTablePrefix() . 'group_membership', array('group_name'))
- ->where('username = ?', $user->getUsername())
- ->query();
- foreach ($membershipsStmt as $membership) {
- $memberships[] = $membership->group_name;
- $parent = $groups[$membership->group_name];
- while (isset($parent)) {
- $memberships[] = $parent;
- $parent = $groups[$parent];
- }
- }
- return $memberships;
- }
-}
diff --git a/library/Icinga/Authentication/Backend/IniUserGroupBackend.php b/library/Icinga/Authentication/Backend/IniUserGroupBackend.php
deleted file mode 100644
index b7f366511..000000000
--- a/library/Icinga/Authentication/Backend/IniUserGroupBackend.php
+++ /dev/null
@@ -1,64 +0,0 @@
-config = $config;
- }
-
- /**
- * (non-PHPDoc)
- * @see UserGroupBackend::getMemberships() For the method documentation.
- */
- public function getMemberships(User $user)
- {
- $username = strtolower($user->getUsername());
- $groups = array();
- foreach ($this->config as $name => $section) {
- if (empty($section->users)) {
- throw new ConfigurationError(
- 'Membership section \'%s\' in \'%s\' is missing the \'users\' section',
- $name,
- $this->config->getConfigFile()
- );
- }
- if (empty($section->groups)) {
- throw new ConfigurationError(
- 'Membership section \'%s\' in \'%s\' is missing the \'groups\' section',
- $name,
- $this->config->getConfigFile()
- );
- }
- $users = array_map('strtolower', String::trimSplit($section->users));
- if (in_array($username, $users)) {
- $groups = array_merge($groups, array_diff(String::trimSplit($section->groups), $groups));
- }
- }
- return $groups;
- }
-}
diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php
deleted file mode 100644
index 858fa742e..000000000
--- a/library/Icinga/Authentication/Backend/LdapUserBackend.php
+++ /dev/null
@@ -1,291 +0,0 @@
- 'uid',
- 'user' => 'user',
- 'inetorgperson' => 'inetOrgPerson',
- 'samaccountname' => 'sAMAccountName'
- );
-
- public function __construct(
- Connection $conn,
- $userClass,
- $userNameAttribute,
- $baseDn,
- $cutomFilter,
- $groupOptions = null
- ) {
- $this->conn = $conn;
- $this->baseDn = trim($baseDn) ?: $conn->getDN();
- $this->userClass = $this->getNormedAttribute($userClass);
- $this->userNameAttribute = $this->getNormedAttribute($userNameAttribute);
- $this->customFilter = trim($cutomFilter);
- $this->groupOptions = $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;
- }
-
- /**
- * Create a query to select all usernames
- *
- * @return Query
- */
- protected function selectUsers()
- {
- $query = $this->conn->select()->setBase($this->baseDn)->from(
- $this->userClass,
- array(
- $this->userNameAttribute
- )
- );
-
- if ($this->customFilter) {
- $query->addFilter(new Expression($this->customFilter));
- }
-
- return $query;
- }
-
- /**
- * Create a query filtered by the given username
- *
- * @param string $username
- *
- * @return Query
- */
- protected function selectUser($username)
- {
- return $this->selectUsers()->setUsePagedResults(false)->where(
- $this->userNameAttribute,
- str_replace('*', '', $username)
- );
- }
-
- /**
- * Probe the backend to test if authentication is possible
- *
- * Try to bind to the backend and query all available users to check if:
- *
- * - Connection credentials are correct and the bind is possible
- * - At least one user exists
- * - The specified userClass has the property specified by userNameAttribute
- *
- *
- * @throws AuthenticationException When authentication is not possible
- */
- public function assertAuthenticationPossible()
- {
- try {
- $result = $this->selectUsers()->fetchRow();
- } catch (LdapException $e) {
- throw new AuthenticationException('Connection not possible.', $e);
- }
-
- if ($result === null) {
- throw new AuthenticationException(
- 'No objects with objectClass="%s" in DN="%s" found. (Filter: %s)',
- $this->userClass,
- $this->baseDn,
- $this->customFilter ?: 'None'
- );
- }
-
- if (! isset($result->{$this->userNameAttribute})) {
- throw new AuthenticationException(
- 'UserNameAttribute "%s" not existing in objectClass="%s"',
- $this->userNameAttribute,
- $this->userClass
- );
- }
- }
-
- /**
- * 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();
- }
-
- $q = $this->conn->select()
- ->setBase($this->groupOptions['group_base_dn'])
- ->from(
- $this->groupOptions['group_class'],
- array($this->groupOptions['group_attribute'])
- )
- ->where(
- $this->groupOptions['group_member_attribute'],
- $dn
- );
-
- $result = $this->conn->fetchAll($q);
-
- $groups = array();
-
- foreach ($result as $group) {
- $groups[] = $group->{$this->groupOptions['group_attribute']};
- }
-
- return $groups;
- }
-
- /**
- * Return whether the given user exists
- *
- * @param User $user
- *
- * @return bool
- */
- public function hasUser(User $user)
- {
- $username = $user->getUsername();
- $entry = $this->selectUser($username)->fetchOne();
-
- if (is_array($entry)) {
- return in_array(strtolower($username), array_map('strtolower', $entry));
- }
-
- return strtolower($entry) === strtolower($username);
- }
-
- /**
- * Return whether the given user credentials are valid
- *
- * @param User $user
- * @param string $password
- * @param boolean $healthCheck Assert that authentication is possible at all
- *
- * @return bool
- *
- * @throws AuthenticationException In case an error occured or the health check has failed
- */
- public function authenticate(User $user, $password, $healthCheck = false)
- {
- if ($healthCheck) {
- try {
- $this->assertAuthenticationPossible();
- } catch (AuthenticationException $e) {
- throw new AuthenticationException(
- 'Authentication against backend "%s" not possible.',
- $this->getName(),
- $e
- );
- }
- }
-
- if (! $this->hasUser($user)) {
- return false;
- }
-
- try {
- $userDn = $this->conn->fetchDN($this->selectUser($user->getUsername()));
- $authenticated = $this->conn->testCredentials(
- $userDn,
- $password
- );
-
- if ($authenticated) {
- $groups = $this->getGroups($userDn);
- if ($groups !== null) {
- $user->setGroups($groups);
- }
- }
-
- return $authenticated;
- } catch (LdapException $e) {
- throw new AuthenticationException(
- 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
- $user->getUsername(),
- $this->getName(),
- $e
- );
- }
- }
-
- /**
- * Get the number of users available
- *
- * @return int
- */
- public function count()
- {
- return $this->selectUsers()->count();
- }
-
- /**
- * Return the names of all available users
- *
- * @return array
- */
- public function listUsers()
- {
- $users = array();
- foreach ($this->selectUsers()->fetchAll() as $row) {
- if (is_array($row->{$this->userNameAttribute})) {
- foreach ($row->{$this->userNameAttribute} as $col) {
- $users[] = $col;
- }
- } else {
- $users[] = $row->{$this->userNameAttribute};
- }
- }
- return $users;
- }
-}
diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php
index 5705b493b..fed012ab5 100644
--- a/library/Icinga/Authentication/Manager.php
+++ b/library/Icinga/Authentication/Manager.php
@@ -4,6 +4,7 @@
namespace Icinga\Authentication;
use Exception;
+use Icinga\Authentication\UserGroup\UserGroupBackend;
use Icinga\Application\Config;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotReadableError;
@@ -55,7 +56,7 @@ class Manager
} catch (NotReadableError $e) {
Logger::error(
new IcingaException(
- 'Cannot load preferences for user "%s". An exception was thrown',
+ 'Cannot load preferences for user "%s". An exception was thrown: %s',
$username,
$e
)
@@ -73,7 +74,7 @@ class Manager
} catch (Exception $e) {
Logger::error(
new IcingaException(
- 'Cannot load preferences for user "%s". An exception was thrown',
+ 'Cannot load preferences for user "%s". An exception was thrown: %s',
$username,
$e
)
@@ -91,7 +92,7 @@ class Manager
$groupsFromBackend = $groupBackend->getMemberships($user);
} catch (Exception $e) {
Logger::error(
- 'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown:',
+ 'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s',
$username,
$name,
$e
diff --git a/library/Icinga/Authentication/User/DbUserBackend.php b/library/Icinga/Authentication/User/DbUserBackend.php
new file mode 100644
index 000000000..0e5dad5fa
--- /dev/null
+++ b/library/Icinga/Authentication/User/DbUserBackend.php
@@ -0,0 +1,249 @@
+ array(
+ 'user' => 'name COLLATE utf8_general_ci',
+ 'user_name' => 'name',
+ 'is_active' => 'active',
+ 'created_at' => 'UNIX_TIMESTAMP(ctime)',
+ 'last_modified' => 'UNIX_TIMESTAMP(mtime)'
+ )
+ );
+
+ /**
+ * The statement columns being provided
+ *
+ * @var array
+ */
+ protected $statementColumns = array(
+ 'user' => array(
+ 'password' => 'password_hash',
+ 'created_at' => 'ctime',
+ 'last_modified' => 'mtime'
+ )
+ );
+
+ /**
+ * The columns which are not permitted to be queried
+ *
+ * @var array
+ */
+ protected $filterColumns = array('user');
+
+ /**
+ * The default sort rules to be applied on a query
+ *
+ * @var array
+ */
+ protected $sortRules = array(
+ 'user_name' => array(
+ 'columns' => array(
+ 'is_active desc',
+ 'user_name'
+ )
+ )
+ );
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * @var array
+ */
+ protected $conversionRules = array(
+ 'user' => array(
+ 'password'
+ )
+ );
+
+ /**
+ * Initialize this database user backend
+ */
+ protected function init()
+ {
+ if (! $this->ds->getTablePrefix()) {
+ $this->ds->setTablePrefix('icingaweb_');
+ }
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * @param string $table
+ * @param array $bind
+ */
+ public function insert($table, array $bind)
+ {
+ $bind['created_at'] = date('Y-m-d H:i:s');
+ $this->ds->insert(
+ $this->prependTablePrefix($table),
+ $this->requireStatementColumns($table, $bind),
+ array(
+ 'active' => PDO::PARAM_INT,
+ 'password_hash' => PDO::PARAM_LOB
+ )
+ );
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ */
+ public function update($table, array $bind, Filter $filter = null)
+ {
+ $bind['last_modified'] = date('Y-m-d H:i:s');
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ $this->ds->update(
+ $this->prependTablePrefix($table),
+ $this->requireStatementColumns($table, $bind),
+ $filter,
+ array(
+ 'active' => PDO::PARAM_INT,
+ 'password_hash' => PDO::PARAM_LOB
+ )
+ );
+ }
+
+ /**
+ * Hash and return the given password
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ protected function persistPassword($value)
+ {
+ return $this->hashPassword($value);
+ }
+
+ /**
+ * Fetch the hashed password for the given user
+ *
+ * @param string $username The name of the user
+ *
+ * @return string
+ */
+ protected function getPasswordHash($username)
+ {
+ if ($this->ds->getDbType() === 'pgsql') {
+ // Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
+ $columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')');
+ } else {
+ $columns = array('password_hash');
+ }
+
+ $query = $this->ds->select()
+ ->from($this->prependTablePrefix('user'), $columns)
+ ->where('name', $username)
+ ->where('active', true);
+ $statement = $this->ds->getDbAdapter()->prepare($query->getSelectQuery());
+ $statement->execute();
+ $statement->bindColumn(1, $lob, PDO::PARAM_LOB);
+ $statement->fetch(PDO::FETCH_BOUND);
+ if (is_resource($lob)) {
+ $lob = stream_get_contents($lob);
+ }
+
+ return $this->ds->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob;
+ }
+
+ /**
+ * Authenticate the given user
+ *
+ * @param User $user
+ * @param string $password
+ *
+ * @return bool True on success, false on failure
+ *
+ * @throws AuthenticationException In case authentication is not possible due to an error
+ */
+ public function authenticate(User $user, $password)
+ {
+ try {
+ $passwordHash = $this->getPasswordHash($user->getUsername());
+ $passwordSalt = $this->getSalt($passwordHash);
+ $hashToCompare = $this->hashPassword($password, $passwordSalt);
+ return $hashToCompare === $passwordHash;
+ } catch (Exception $e) {
+ throw new AuthenticationException(
+ 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
+ $user->getUsername(),
+ $this->getName(),
+ $e
+ );
+ }
+ }
+
+ /**
+ * Extract salt from the given password hash
+ *
+ * @param string $hash The hashed password
+ *
+ * @return string
+ */
+ protected function getSalt($hash)
+ {
+ return substr($hash, strlen(self::HASH_ALGORITHM), self::SALT_LENGTH);
+ }
+
+ /**
+ * Return a random salt
+ *
+ * The returned salt is safe to be used for hashing a user's password
+ *
+ * @return string
+ */
+ protected function generateSalt()
+ {
+ return openssl_random_pseudo_bytes(self::SALT_LENGTH);
+ }
+
+ /**
+ * Hash a password
+ *
+ * @param string $password
+ * @param string $salt
+ *
+ * @return string
+ */
+ protected function hashPassword($password, $salt = null)
+ {
+ return crypt($password, self::HASH_ALGORITHM . ($salt !== null ? $salt : $this->generateSalt()));
+ }
+}
diff --git a/library/Icinga/Authentication/Backend/ExternalBackend.php b/library/Icinga/Authentication/User/ExternalBackend.php
similarity index 64%
rename from library/Icinga/Authentication/Backend/ExternalBackend.php
rename to library/Icinga/Authentication/User/ExternalBackend.php
index fb73c55e8..413c0553b 100644
--- a/library/Icinga/Authentication/Backend/ExternalBackend.php
+++ b/library/Icinga/Authentication/User/ExternalBackend.php
@@ -1,23 +1,29 @@
name = $name;
+ return $this;
}
/**
- * Test whether the given user exists
+ * Return this backend's name
*
- * @param User $user
- *
- * @return bool
+ * @return string
*/
- public function hasUser(User $user)
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Authenticate the given user
+ *
+ * @param User $user
+ * @param string $password
+ *
+ * @return bool True on success, false on failure
+ *
+ * @throws AuthenticationException In case authentication is not possible due to an error
+ */
+ public function authenticate(User $user, $password = null)
{
if (isset($_SERVER['REMOTE_USER'])) {
$username = $_SERVER['REMOTE_USER'];
$user->setRemoteUserInformation($username, 'REMOTE_USER');
+
if ($this->stripUsernameRegexp) {
$stripped = preg_replace($this->stripUsernameRegexp, '', $username);
if ($stripped !== false) {
@@ -61,23 +82,11 @@ class ExternalBackend extends UserBackend
$username = $stripped;
}
}
+
$user->setUsername($username);
return true;
}
return false;
}
-
- /**
- * Authenticate
- *
- * @param User $user
- * @param string $password
- *
- * @return bool
- */
- public function authenticate(User $user, $password = null)
- {
- return $this->hasUser($user);
- }
}
diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php
new file mode 100644
index 000000000..154a33a44
--- /dev/null
+++ b/library/Icinga/Authentication/User/LdapUserBackend.php
@@ -0,0 +1,493 @@
+ array(
+ 'columns' => array(
+ 'is_active desc',
+ 'user_name'
+ )
+ )
+ );
+
+ 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
+ *
+ * @param string $baseDn
+ *
+ * @return $this
+ */
+ public function setBaseDn($baseDn)
+ {
+ if (($baseDn = trim($baseDn))) {
+ $this->baseDn = $baseDn;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the base DN to use for a query
+ *
+ * @return string
+ */
+ public function getBaseDn()
+ {
+ return $this->baseDn;
+ }
+
+ /**
+ * Set the objectClass where to look for users
+ *
+ * Sets also the base table name for the underlying repository.
+ *
+ * @param string $userClass
+ *
+ * @return $this
+ */
+ public function setUserClass($userClass)
+ {
+ $this->baseTable = $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 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 custom LDAP filter to apply on search queries
+ *
+ * @param string $filter
+ *
+ * @return $this
+ */
+ public function setFilter($filter)
+ {
+ if (($filter = trim($filter))) {
+ $this->filter = $filter;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Return the custom LDAP filter to apply on search queries
+ *
+ * @return string
+ */
+ public function getFilter()
+ {
+ 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
+ *
+ * @param ConfigObject $config
+ *
+ * @return $this
+ */
+ public function setConfig(ConfigObject $config)
+ {
+ return $this
+ ->setBaseDn($config->base_dn)
+ ->setUserClass($config->user_class)
+ ->setUserNameAttribute($config->user_name_attribute)
+ ->setFilter($config->filter);
+ }
+
+ /**
+ * 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->baseDn);
+ if ($this->filter) {
+ $query->getQuery()->where(new Expression($this->filter));
+ }
+
+ return $query;
+ }
+
+ /**
+ * Initialize this repository's query columns
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case either $this->userNameAttribute or $this->userClass has not been set yet
+ */
+ protected function initializeQueryColumns()
+ {
+ if ($this->userClass === null) {
+ throw new ProgrammingError('It is required to set the objectClass where to look for users first');
+ }
+ if ($this->userNameAttribute === null) {
+ throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first');
+ }
+
+ if ($this->ds->getCapabilities()->hasAdOid()) {
+ $isActiveAttribute = 'userAccountControl';
+ $createdAtAttribute = 'whenCreated';
+ $lastModifiedAttribute = 'whenChanged';
+ } else {
+ // TODO(jom): Elaborate whether it is possible to add dynamic support for the ppolicy
+ $isActiveAttribute = 'shadowExpire';
+
+ $createdAtAttribute = 'createTimestamp';
+ $lastModifiedAttribute = 'modifyTimestamp';
+ }
+
+ return array(
+ $this->userClass => array(
+ 'user' => $this->userNameAttribute,
+ 'user_name' => $this->userNameAttribute,
+ 'is_active' => $isActiveAttribute,
+ 'created_at' => $createdAtAttribute,
+ 'last_modified' => $lastModifiedAttribute
+ )
+ );
+ }
+
+ /**
+ * Initialize this repository's conversion rules
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $this->userClass has not been set yet
+ */
+ protected function initializeConversionRules()
+ {
+ if ($this->userClass === null) {
+ throw new ProgrammingError('It is required to set the objectClass where to look for users first');
+ }
+
+ if ($this->ds->getCapabilities()->hasAdOid()) {
+ $stateConverter = 'user_account_control';
+ } else {
+ $stateConverter = 'shadow_expire';
+ }
+
+ return array(
+ $this->userClass => array(
+ 'is_active' => $stateConverter,
+ 'created_at' => 'generalized_time',
+ 'last_modified' => 'generalized_time'
+ )
+ );
+ }
+
+ /**
+ * Return whether the given userAccountControl value defines that a user is permitted to login
+ *
+ * @param string|null $value
+ *
+ * @return bool
+ */
+ protected function retrieveUserAccountControl($value)
+ {
+ if ($value === null) {
+ return $value;
+ }
+
+ $ADS_UF_ACCOUNTDISABLE = 2;
+ 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
+ *
+ * @param string|null $value
+ *
+ * @return bool
+ */
+ protected function retrieveShadowExpire($value)
+ {
+ if ($value === null) {
+ return $value;
+ }
+
+ $now = new DateTime();
+ $bigBang = clone $now;
+ $bigBang->setTimestamp(0);
+ return ((int) $value) >= $bigBang->diff($now)->days;
+ }
+
+ /**
+ * Probe the backend to test if authentication is possible
+ *
+ * Try to bind to the backend and fetch a single user to check if:
+ *
+ * - Connection credentials are correct and the bind is possible
+ * - At least one user exists
+ * - The specified userClass has the property specified by userNameAttribute
+ *
+ *
+ * @throws AuthenticationException When authentication is not possible
+ */
+ public function assertAuthenticationPossible()
+ {
+ try {
+ $result = $this->select()->fetchRow();
+ } catch (LdapException $e) {
+ throw new AuthenticationException('Connection not possible.', $e);
+ }
+
+ if ($result === null) {
+ throw new AuthenticationException(
+ 'No objects with objectClass "%s" in DN "%s" found. (Filter: %s)',
+ $this->userClass,
+ $this->baseDn ?: $this->ds->getDn(),
+ $this->filter ?: 'None'
+ );
+ }
+
+ if (! isset($result->user_name)) {
+ throw new AuthenticationException(
+ 'UserNameAttribute "%s" not existing in objectClass "%s"',
+ $this->userNameAttribute,
+ $this->userClass
+ );
+ }
+ }
+
+ /**
+ * 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
+ *
+ * @param User $user
+ * @param string $password
+ *
+ * @return bool True on success, false on failure
+ *
+ * @throws AuthenticationException In case authentication is not possible due to an error
+ */
+ public function authenticate(User $user, $password)
+ {
+ try {
+ $userDn = $this
+ ->select()
+ ->where('user_name', str_replace('*', '', $user->getUsername()))
+ ->getQuery()
+ ->setUsePagedResults(false)
+ ->fetchDn();
+
+ if ($userDn === null) {
+ return false;
+ }
+
+ $authenticated = $this->ds->testCredentials($userDn, $password);
+ if ($authenticated) {
+ $groups = $this->getGroups($userDn);
+ if ($groups !== null) {
+ $user->setGroups($groups);
+ }
+ }
+
+ return $authenticated;
+ } catch (LdapException $e) {
+ throw new AuthenticationException(
+ 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
+ $user->getUsername(),
+ $this->getName(),
+ $e
+ );
+ }
+ }
+}
diff --git a/library/Icinga/Authentication/User/UserBackend.php b/library/Icinga/Authentication/User/UserBackend.php
new file mode 100644
index 000000000..3d11289fb
--- /dev/null
+++ b/library/Icinga/Authentication/User/UserBackend.php
@@ -0,0 +1,193 @@
+getModuleManager()->getLoadedModules() as $module) {
+ foreach ($module->getUserBackends() as $identifier => $className) {
+ if (array_key_exists($identifier, $providedBy)) {
+ Logger::warning(
+ 'Cannot register user backend of type "%s" provided by module "%s".'
+ . ' The type is already provided by module "%s"',
+ $identifier,
+ $module->getName(),
+ $providedBy[$identifier]
+ );
+ } elseif (in_array($identifier, static::$defaultBackends)) {
+ Logger::warning(
+ 'Cannot register user backend of type "%s" provided by module "%s".'
+ . ' The type is a default type provided by Icinga Web 2',
+ $identifier,
+ $module->getName()
+ );
+ } else {
+ $providedBy[$identifier] = $module->getName();
+ static::$customBackends[$identifier] = $className;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the class for the given custom user backend
+ *
+ * @param string $identifier The identifier of the custom user backend
+ *
+ * @return string|null The name of the class or null in case there was no
+ * backend found with the given identifier
+ *
+ * @throws ConfigurationError In case the class associated to the given identifier does not exist
+ */
+ protected static function getCustomUserBackend($identifier)
+ {
+ static::registerCustomUserBackends();
+ if (array_key_exists($identifier, static::$customBackends)) {
+ $className = static::$customBackends[$identifier];
+ if (! class_exists($className)) {
+ throw new ConfigurationError(
+ 'Cannot utilize user backend of type "%s". Class "%s" does not exist',
+ $identifier,
+ $className
+ );
+ }
+
+ return $className;
+ }
+ }
+
+ /**
+ * Create and return a user backend with the given name and given configuration applied to it
+ *
+ * @param string $name
+ * @param ConfigObject $backendConfig
+ *
+ * @return UserBackendInterface
+ *
+ * @throws ConfigurationError
+ */
+ public static function create($name, ConfigObject $backendConfig)
+ {
+ if ($backendConfig->name !== null) {
+ $name = $backendConfig->name;
+ }
+
+ if (! ($backendType = strtolower($backendConfig->backend))) {
+ throw new ConfigurationError(
+ 'Authentication configuration for user backend "%s" is missing the \'backend\' directive',
+ $name
+ );
+ }
+ if ($backendType === 'external') {
+ $backend = new ExternalBackend($backendConfig);
+ $backend->setName($name);
+ return $backend;
+ }
+ if (in_array($backendType, static::$defaultBackends)) {
+ // The default backend check is the first one because of performance reasons:
+ // Do not attempt to load a custom user backend unless it's actually required
+ } elseif (($customClass = static::getCustomUserBackend($backendType)) !== null) {
+ $backend = new $customClass($backendConfig);
+ if (! is_a($backend, 'Icinga\Authentication\User\UserBackendInterface')) {
+ throw new ConfigurationError(
+ 'Cannot utilize user backend of type "%s". Class "%s" does not implement UserBackendInterface',
+ $backendType,
+ $customClass
+ );
+ }
+
+ $backend->setName($name);
+ return $backend;
+ } else {
+ throw new ConfigurationError(
+ 'Authentication configuration for user backend "%s" defines an invalid backend type.'
+ . ' Backend type "%s" is not supported',
+ $name,
+ $backendType
+ );
+ }
+
+ if ($backendConfig->resource === null) {
+ throw new ConfigurationError(
+ 'Authentication configuration for user backend "%s" is missing the \'resource\' directive',
+ $name
+ );
+ }
+ $resource = ResourceFactory::create($backendConfig->resource);
+
+ switch ($backendType) {
+ case 'db':
+ $backend = new DbUserBackend($resource);
+ break;
+ case 'msldap':
+ $backend = new LdapUserBackend($resource);
+ $backend->setBaseDn($backendConfig->base_dn);
+ $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);
+ $backend->setBaseDn($backendConfig->base_dn);
+ $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;
+ }
+
+ $backend->setName($name);
+ return $backend;
+ }
+}
diff --git a/library/Icinga/Authentication/User/UserBackendInterface.php b/library/Icinga/Authentication/User/UserBackendInterface.php
new file mode 100644
index 000000000..cfb2e3753
--- /dev/null
+++ b/library/Icinga/Authentication/User/UserBackendInterface.php
@@ -0,0 +1,41 @@
+name = $name;
- return $this;
- }
-
- /**
- * Getter for the backend's name
- *
- * @return string
- */
- public function getName()
- {
- return $this->name;
- }
-
- public static function create($name, ConfigObject $backendConfig)
- {
- if ($backendConfig->name !== null) {
- $name = $backendConfig->name;
- }
- if (isset($backendConfig->class)) {
- // Use a custom backend class, this is only useful for testing
- if (!class_exists($backendConfig->class)) {
- throw new ConfigurationError(
- 'Authentication configuration for backend "%s" defines an invalid backend class.'
- . ' Backend class "%s" not found',
- $name,
- $backendConfig->class
- );
- }
- return new $backendConfig->class($backendConfig);
- }
- if (($backendType = $backendConfig->backend) === null) {
- throw new ConfigurationError(
- 'Authentication configuration for backend "%s" is missing the backend directive',
- $name
- );
- }
- $backendType = strtolower($backendType);
- if ($backendType === 'external') {
- $backend = new ExternalBackend($backendConfig);
- $backend->setName($name);
- return $backend;
- }
- if ($backendConfig->resource === null) {
- throw new ConfigurationError(
- 'Authentication configuration for backend "%s" is missing the resource directive',
- $name
- );
- }
- try {
- $resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource);
- } catch (ProgrammingError $e) {
- throw new ConfigurationError(
- 'Resources not set up. Please contact your Icinga Web administrator'
- );
- }
- $resource = ResourceFactory::createResource($resourceConfig);
- switch ($backendType) {
- case 'db':
- $backend = new DbUserBackend($resource);
- break;
- case 'msldap':
- $groupOptions = 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')
- );
- $backend = new LdapUserBackend(
- $resource,
- $backendConfig->get('user_class', 'user'),
- $backendConfig->get('user_name_attribute', 'sAMAccountName'),
- $backendConfig->get('base_dn', $resource->getDN()),
- $backendConfig->get('filter'),
- $groupOptions
- );
- break;
- case 'ldap':
- if ($backendConfig->user_class === null) {
- throw new ConfigurationError(
- 'Authentication configuration for backend "%s" is missing the user_class directive',
- $name
- );
- }
- if ($backendConfig->user_name_attribute === null) {
- throw new ConfigurationError(
- 'Authentication configuration for backend "%s" is missing the user_name_attribute directive',
- $name
- );
- }
- $groupOptions = 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
- );
- $backend = new LdapUserBackend(
- $resource,
- $backendConfig->user_class,
- $backendConfig->user_name_attribute,
- $backendConfig->get('base_dn', $resource->getDN()),
- $backendConfig->get('filter'),
- $groupOptions
- );
- break;
- default:
- throw new ConfigurationError(
- 'Authentication configuration for backend "%s" defines an invalid backend type.'
- . ' Backend type "%s" is not supported',
- $name,
- $backendType
- );
- }
- $backend->setName($name);
- return $backend;
- }
-
- /**
- * Test whether the given user exists
- *
- * @param User $user
- *
- * @return bool
- */
- abstract public function hasUser(User $user);
-
- /**
- * Authenticate
- *
- * @param User $user
- * @param string $password
- *
- * @return bool
- */
- abstract public function authenticate(User $user, $password);
-}
diff --git a/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
new file mode 100644
index 000000000..e59b03258
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/DbUserGroupBackend.php
@@ -0,0 +1,264 @@
+ array(
+ 'group_id' => 'g.id',
+ 'group' => 'g.name COLLATE utf8_general_ci',
+ 'group_name' => 'g.name',
+ 'parent' => 'g.parent',
+ 'created_at' => 'UNIX_TIMESTAMP(g.ctime)',
+ 'last_modified' => 'UNIX_TIMESTAMP(g.mtime)'
+ ),
+ 'group_membership' => array(
+ 'group_id' => 'gm.group_id',
+ 'user' => 'gm.username COLLATE utf8_general_ci',
+ 'user_name' => 'gm.username',
+ 'created_at' => 'UNIX_TIMESTAMP(gm.ctime)',
+ 'last_modified' => 'UNIX_TIMESTAMP(gm.mtime)'
+ )
+ );
+
+ /**
+ * The table aliases being applied
+ *
+ * @var array
+ */
+ protected $tableAliases = array(
+ 'group' => 'g',
+ 'group_membership' => 'gm'
+ );
+
+ /**
+ * The statement columns being provided
+ *
+ * @var array
+ */
+ protected $statementColumns = array(
+ 'group' => array(
+ 'group_id' => 'id',
+ 'group_name' => 'name',
+ 'parent' => 'parent',
+ 'created_at' => 'ctime',
+ 'last_modified' => 'mtime'
+ ),
+ 'group_membership' => array(
+ 'group_id' => 'group_id',
+ 'group_name' => 'group_id',
+ 'user_name' => 'username',
+ 'created_at' => 'ctime',
+ 'last_modified' => 'mtime'
+ )
+ );
+
+ /**
+ * The columns which are not permitted to be queried
+ *
+ * @var array
+ */
+ protected $filterColumns = array('group', 'user');
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * @var array
+ */
+ protected $conversionRules = array(
+ 'group' => array(
+ 'parent' => 'group_id'
+ ),
+ 'group_membership' => array(
+ 'group_name' => 'group_id'
+ )
+ );
+
+ /**
+ * Initialize this database user group backend
+ */
+ protected function init()
+ {
+ if (! $this->ds->getTablePrefix()) {
+ $this->ds->setTablePrefix('icingaweb_');
+ }
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * @param string $table
+ * @param array $bind
+ */
+ public function insert($table, array $bind)
+ {
+ $bind['created_at'] = date('Y-m-d H:i:s');
+ parent::insert($table, $bind);
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ */
+ public function update($table, array $bind, Filter $filter = null)
+ {
+ $bind['last_modified'] = date('Y-m-d H:i:s');
+ parent::update($table, $bind, $filter);
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ if ($table === 'group') {
+ parent::delete('group_membership', $filter);
+ $idQuery = $this->select(array('group_id'));
+ if ($filter !== null) {
+ $idQuery->applyFilter($filter);
+ }
+
+ $this->update('group', array('parent' => null), Filter::where('parent', $idQuery->fetchColumn()));
+ }
+
+ parent::delete($table, $filter);
+ }
+
+ /**
+ * Return the groups the given user is a member of
+ *
+ * @param User $user
+ *
+ * @return array
+ */
+ public function getMemberships(User $user)
+ {
+ $groupQuery = $this->ds
+ ->select()
+ ->from(
+ array('g' => $this->prependTablePrefix('group')),
+ array(
+ 'group_name' => 'g.name',
+ 'parent_name' => 'gg.name'
+ )
+ )->joinLeft(
+ array('gg' => $this->prependTablePrefix('group')),
+ 'g.parent = gg.id',
+ array()
+ );
+
+ $groups = array();
+ foreach ($groupQuery as $group) {
+ $groups[$group->group_name] = $group->parent_name;
+ }
+
+ $membershipQuery = $this
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $user->getUsername());
+
+ $memberships = array();
+ foreach ($membershipQuery as $membership) {
+ $memberships[] = $membership->group_name;
+ $parent = $groups[$membership->group_name];
+ while ($parent !== null) {
+ $memberships[] = $parent;
+ // Usually a parent is an existing group, but since we do not have a constraint on our table..
+ $parent = isset($groups[$parent]) ? $groups[$parent] : null;
+ }
+ }
+
+ return $memberships;
+ }
+
+ /**
+ * Join group into group_membership
+ *
+ * @param RepositoryQuery $query
+ */
+ protected function joinGroup(RepositoryQuery $query)
+ {
+ $query->getQuery()->join(
+ $this->requireTable('group'),
+ 'gm.group_id = g.id',
+ array()
+ );
+ }
+
+ /**
+ * Join group_membership into group
+ *
+ * @param RepositoryQuery $query
+ */
+ protected function joinGroupMembership(RepositoryQuery $query)
+ {
+ $query->getQuery()->join(
+ $this->requireTable('group_membership'),
+ 'g.id = gm.group_id',
+ array()
+ );
+ }
+
+ /**
+ * Fetch and return the corresponding id for the given group's name
+ *
+ * @param string|array $groupName
+ *
+ * @return int
+ *
+ * @throws NotFoundError
+ */
+ protected function persistGroupId($groupName)
+ {
+ if (! $groupName || empty($groupName) || is_int($groupName)) {
+ return $groupName;
+ }
+
+ if (is_array($groupName)) {
+ if (is_int($groupName[0])) {
+ return $groupName; // In case the array contains mixed types...
+ }
+
+ $groupIds = $this->ds
+ ->select()
+ ->from($this->prependTablePrefix('group'), array('id'))
+ ->where('name', $groupName)
+ ->fetchColumn();
+ if (empty($groupIds)) {
+ throw new NotFoundError('No groups found matching one of: %s', implode(', ', $groupName));
+ }
+
+ return $groupIds;
+ }
+
+ $groupId = $this->ds
+ ->select()
+ ->from($this->prependTablePrefix('group'), array('id'))
+ ->where('name', $groupName)
+ ->fetchOne();
+ if ($groupId === false) {
+ throw new NotFoundError('Group "%s" does not exist', $groupName);
+ }
+
+ return $groupId;
+ }
+}
diff --git a/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php
new file mode 100644
index 000000000..53eadc147
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/IniUserGroupBackend.php
@@ -0,0 +1,121 @@
+ array(
+ 'group' => 'name',
+ 'group_name' => 'name',
+ 'parent' => 'parent',
+ 'created_at' => 'ctime',
+ 'last_modified' => 'mtime',
+ 'users'
+ )
+ );
+
+ /**
+ * The columns which are not permitted to be queried
+ *
+ * @var array
+ */
+ protected $filterColumns = array('group');
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * @var array
+ */
+ protected $conversionRules = array(
+ 'groups' => array(
+ 'created_at' => 'date_time',
+ 'last_modified' => 'date_time',
+ 'users' => 'comma_separated_string'
+ )
+ );
+
+ /**
+ * Initialize this ini user group backend
+ */
+ protected function init()
+ {
+ $this->ds->getConfigObject()->setKeyColumn('name');
+ }
+
+ /**
+ * Add a new group to this backend
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function insert($target, array $data)
+ {
+ $data['created_at'] = time();
+ parent::insert($target, $data);
+ }
+
+ /**
+ * Update groups of this backend, optionally limited using a filter
+ *
+ * @param string $target
+ * @param array $data
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function update($target, array $data, Filter $filter = null)
+ {
+ $data['last_modified'] = time();
+ parent::update($target, $data, $filter);
+ }
+
+ /**
+ * Return the groups the given user is a member of
+ *
+ * @param User $user
+ *
+ * @return array
+ */
+ public function getMemberships(User $user)
+ {
+ $result = $this->select()->fetchAll();
+
+ $groups = array();
+ foreach ($result as $group) {
+ $groups[$group->group_name] = $group->parent;
+ }
+
+ $username = strtolower($user->getUsername());
+ $memberships = array();
+ foreach ($result as $group) {
+ if ($group->users && !in_array($group->group_name, $memberships)) {
+ $users = array_map('strtolower', String::trimSplit($group->users));
+ if (in_array($username, $users)) {
+ $memberships[] = $group->group_name;
+ $parent = $groups[$group->group_name];
+ while ($parent !== null) {
+ $memberships[] = $parent;
+ $parent = isset($groups[$parent]) ? $groups[$parent] : null;
+ }
+ }
+ }
+ }
+
+ return $memberships;
+ }
+}
diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackend.php b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
new file mode 100644
index 000000000..dd4900ea8
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/UserGroupBackend.php
@@ -0,0 +1,164 @@
+getModuleManager()->getLoadedModules() as $module) {
+ foreach ($module->getUserGroupBackends() as $identifier => $className) {
+ if (array_key_exists($identifier, $providedBy)) {
+ Logger::warning(
+ 'Cannot register user group backend of type "%s" provided by module "%s".'
+ . ' The type is already provided by module "%s"',
+ $identifier,
+ $module->getName(),
+ $providedBy[$identifier]
+ );
+ } elseif (in_array($identifier, static::$defaultBackends)) {
+ Logger::warning(
+ 'Cannot register user group backend of type "%s" provided by module "%s".'
+ . ' The type is a default type provided by Icinga Web 2',
+ $identifier,
+ $module->getName()
+ );
+ } else {
+ $providedBy[$identifier] = $module->getName();
+ static::$customBackends[$identifier] = $className;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the class for the given custom user group backend
+ *
+ * @param string $identifier The identifier of the custom user group backend
+ *
+ * @return string|null The name of the class or null in case there was no
+ * backend found with the given identifier
+ *
+ * @throws ConfigurationError In case the class associated to the given identifier does not exist
+ */
+ protected static function getCustomUserGroupBackend($identifier)
+ {
+ static::registerCustomUserGroupBackends();
+ if (array_key_exists($identifier, static::$customBackends)) {
+ $className = static::$customBackends[$identifier];
+ if (! class_exists($className)) {
+ throw new ConfigurationError(
+ 'Cannot utilize user group backend of type "%s". Class "%s" does not exist',
+ $identifier,
+ $className
+ );
+ }
+
+ return $className;
+ }
+ }
+
+ /**
+ * Create and return a user group backend with the given name and given configuration applied to it
+ *
+ * @param string $name
+ * @param ConfigObject $backendConfig
+ *
+ * @return UserGroupBackendInterface
+ *
+ * @throws ConfigurationError
+ */
+ public static function create($name, ConfigObject $backendConfig)
+ {
+ if ($backendConfig->name !== null) {
+ $name = $backendConfig->name;
+ }
+
+ if (! ($backendType = strtolower($backendConfig->backend))) {
+ throw new ConfigurationError(
+ 'Configuration for user group backend "%s" is missing the \'backend\' directive',
+ $name
+ );
+ }
+ if (in_array($backendType, static::$defaultBackends)) {
+ // The default backend check is the first one because of performance reasons:
+ // Do not attempt to load a custom user group backend unless it's actually required
+ } elseif (($customClass = static::getCustomUserGroupBackend($backendType)) !== null) {
+ $backend = new $customClass($backendConfig);
+ if (! is_a($backend, 'Icinga\Authentication\UserGroup\UserGroupBackendInterface')) {
+ throw new ConfigurationError(
+ 'Cannot utilize user group backend of type "%s".'
+ . ' Class "%s" does not implement UserGroupBackendInterface',
+ $backendType,
+ $customClass
+ );
+ }
+
+ $backend->setName($name);
+ return $backend;
+ } else {
+ throw new ConfigurationError(
+ 'Configuration for user group backend "%s" defines an invalid backend type.'
+ . ' Backend type "%s" is not supported',
+ $name,
+ $backendType
+ );
+ }
+
+ if ($backendConfig->resource === null) {
+ throw new ConfigurationError(
+ 'Configuration for user group backend "%s" is missing the \'resource\' directive',
+ $name
+ );
+ }
+ $resource = ResourceFactory::create($backendConfig->resource);
+
+ switch ($backendType) {
+ case 'db':
+ $backend = new DbUserGroupBackend($resource);
+ break;
+ case 'ini':
+ $backend = new IniUserGroupBackend($resource);
+ break;
+ }
+
+ $backend->setName($name);
+ return $backend;
+ }
+}
diff --git a/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php
new file mode 100644
index 000000000..a567d1f0a
--- /dev/null
+++ b/library/Icinga/Authentication/UserGroup/UserGroupBackendInterface.php
@@ -0,0 +1,37 @@
+name = (string) $name;
- return $this;
- }
-
- /**
- * Get the backend name
- *
- * @return string
- */
- public function getName()
- {
- return $this->name;
- }
-
- /**
- * Create a user group backend
- *
- * @param string $name
- * @param ConfigObject $backendConfig
- *
- * @return DbUserGroupBackend|IniUserGroupBackend
- * @throws ConfigurationError If the backend configuration is invalid
- */
- public static function create($name, ConfigObject $backendConfig)
- {
- if ($backendConfig->name !== null) {
- $name = $backendConfig->name;
- }
- if (($backendType = $backendConfig->backend) === null) {
- throw new ConfigurationError(
- 'Configuration for user group backend \'%s\' is missing the \'backend\' directive',
- $name
- );
- }
- $backendType = strtolower($backendType);
- if (($resourceName = $backendConfig->resource) === null) {
- throw new ConfigurationError(
- 'Configuration for user group backend \'%s\' is missing the \'resource\' directive',
- $name
- );
- }
- $resourceName = strtolower($resourceName);
- try {
- $resource = ResourceFactory::create($resourceName);
- } catch (IcingaException $e) {
- throw new ConfigurationError(
- 'Can\'t create user group backend \'%s\'. An exception was thrown: %s',
- $resourceName,
- $e
- );
- }
- switch ($backendType) {
- case 'db':
- $backend = new DbUserGroupBackend($resource);
- break;
- case 'ini':
- $backend = new IniUserGroupBackend($resource);
- break;
- default:
- throw new ConfigurationError(
- 'Can\'t create user group backend \'%s\'. Invalid backend type \'%s\'.',
- $name,
- $backendType
- );
- }
- $backend->setName($name);
- return $backend;
- }
-
- /**
- * Get the groups the given user is a member of
- *
- * @param User $user
- *
- * @return array
- */
- abstract public function getMemberships(User $user);
-}
diff --git a/library/Icinga/Data/ConfigObject.php b/library/Icinga/Data/ConfigObject.php
index 641d72846..de00e5b5e 100644
--- a/library/Icinga/Data/ConfigObject.php
+++ b/library/Icinga/Data/ConfigObject.php
@@ -4,22 +4,15 @@
namespace Icinga\Data;
use Iterator;
-use Countable;
use ArrayAccess;
-use LogicException;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Exception\ProgrammingError;
/**
* Container for configuration values
*/
-class ConfigObject implements Countable, Iterator, ArrayAccess
+class ConfigObject extends ArrayDatasource implements Iterator, ArrayAccess
{
- /**
- * This config's data
- *
- * @var array
- */
- protected $data;
-
/**
* Create a new config
*
@@ -27,15 +20,14 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
*/
public function __construct(array $data = array())
{
- $this->data = array();
-
- foreach ($data as $key => $value) {
+ // Convert all embedded arrays to ConfigObjects as well
+ foreach ($data as & $value) {
if (is_array($value)) {
- $this->data[$key] = new static($value);
- } else {
- $this->data[$key] = $value;
+ $value = new static($value);
}
}
+
+ parent::__construct($data);
}
/**
@@ -55,16 +47,6 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
$this->data = $array;
}
- /**
- * Return the count of available sections and properties
- *
- * @return int
- */
- public function count()
- {
- return count($this->data);
- }
-
/**
* Reset the current position of $this->data
*
@@ -197,7 +179,7 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
public function offsetSet($key, $value)
{
if ($key === null) {
- throw new LogicException('Appending values without an explicit key is not supported');
+ throw new ProgrammingError('Appending values without an explicit key is not supported');
}
$this->$key = $value;
diff --git a/library/Icinga/Data/DataArray/ArrayDatasource.php b/library/Icinga/Data/DataArray/ArrayDatasource.php
index 72ccc4b4c..ef5b4e3ed 100644
--- a/library/Icinga/Data/DataArray/ArrayDatasource.php
+++ b/library/Icinga/Data/DataArray/ArrayDatasource.php
@@ -3,35 +3,105 @@
namespace Icinga\Data\DataArray;
+use ArrayIterator;
use Icinga\Data\Selectable;
use Icinga\Data\SimpleQuery;
class ArrayDatasource implements Selectable
{
+ /**
+ * The array being used as data source
+ *
+ * @var array
+ */
protected $data;
+ /**
+ * The current result
+ *
+ * @var array
+ */
protected $result;
/**
- * Constructor, create a new Datasource for the given Array
+ * The result of a counted query
*
- * @param array $array The array you're going to use as a data source
+ * @var int
*/
- public function __construct(array $array)
+ protected $count;
+
+ /**
+ * The name of the column to map array keys on
+ *
+ * In case the array being used as data source provides keys of type string,this name
+ * will be used to set such as column on each row, if the column is not set already.
+ *
+ * @var string
+ */
+ protected $keyColumn;
+
+ /**
+ * Create a new data source for the given array
+ *
+ * @param array $data The array you're going to use as a data source
+ */
+ public function __construct(array $data)
{
- $this->data = (array) $array;
+ $this->data = $data;
}
/**
- * Instantiate a Query object
+ * Set the name of the column to map array keys on
*
- * @return SimpleQuery
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function setKeyColumn($name)
+ {
+ $this->keyColumn = $name;
+ return $this;
+ }
+
+ /**
+ * Return the name of the column to map array keys on
+ *
+ * @return string
+ */
+ public function getKeyColumn()
+ {
+ return $this->keyColumn;
+ }
+
+ /**
+ * Provide a query for this data source
+ *
+ * @return SimpleQuery
*/
public function select()
{
return new SimpleQuery($this);
}
+ /**
+ * Fetch and return all rows of the given query's result set using an iterator
+ *
+ * @param SimpleQuery $query
+ *
+ * @return ArrayIterator
+ */
+ public function query(SimpleQuery $query)
+ {
+ return new ArrayIterator($this->fetchAll($query));
+ }
+
+ /**
+ * Fetch and return a column of all rows of the result set as an array
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
public function fetchColumn(SimpleQuery $query)
{
$result = array();
@@ -39,9 +109,17 @@ class ArrayDatasource implements Selectable
$arr = (array) $row;
$result[] = array_shift($arr);
}
+
return $result;
}
+ /**
+ * Fetch and return all rows of the given query's result as a flattened key/value based array
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
public function fetchPairs(SimpleQuery $query)
{
$result = array();
@@ -53,104 +131,164 @@ class ArrayDatasource implements Selectable
$keys[1] = $keys[0];
}
}
+
$result[$row->{$keys[0]}] = $row->{$keys[1]};
}
+
return $result;
}
+ /**
+ * Fetch and return the first row of the given query's result
+ *
+ * @param SimpleQuery $query
+ *
+ * @return object|false The row or false in case the result is empty
+ */
public function fetchRow(SimpleQuery $query)
{
$result = $this->getResult($query);
if (empty($result)) {
return false;
}
- return $result[0];
+
+ return array_shift($result);
}
+ /**
+ * Fetch and return all rows of the given query's result as an array
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
public function fetchAll(SimpleQuery $query)
{
return $this->getResult($query);
}
+ /**
+ * Count all rows of the given query's result
+ *
+ * @param SimpleQuery $query
+ *
+ * @return int
+ */
public function count(SimpleQuery $query)
{
- $this->createResult($query);
- return count($this->result);
+ if ($this->count === null) {
+ $this->count = count($this->createResult($query));
+ }
+
+ return $this->count;
}
+ /**
+ * Create and return the result for the given query
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
protected function createResult(SimpleQuery $query)
{
- if ($this->hasResult()) {
- return $this;
- }
- $result = array();
-
$columns = $query->getColumns();
$filter = $query->getFilter();
- foreach ($this->data as & $row) {
+ $offset = $query->hasOffset() ? $query->getOffset() : 0;
+ $limit = $query->hasLimit() ? $query->getLimit() : 0;
+
+ $foundStringKey = false;
+ $result = array();
+ $skipped = 0;
+ foreach ($this->data as $key => $row) {
+ if (is_string($key) && $this->keyColumn !== null && !isset($row->{$this->keyColumn})) {
+ $row = clone $row; // Make sure that this won't affect the actual data
+ $row->{$this->keyColumn} = $key;
+ }
if (! $filter->matches($row)) {
continue;
+ } elseif ($skipped < $offset) {
+ $skipped++;
+ continue;
}
// Get only desired columns if asked so
- if (empty($columns)) {
- $result[] = $row;
- } else {
- $c_row = (object) array();
- foreach ($columns as $alias => $key) {
- if (is_int($alias)) {
- $alias = $key;
+ if (! empty($columns)) {
+ $filteredRow = (object) array();
+ foreach ($columns as $alias => $name) {
+ if (! is_string($alias)) {
+ $alias = $name;
}
- if (isset($row->$key)) {
- $c_row->$alias = $row->$key;
+
+ if (isset($row->$name)) {
+ $filteredRow->$alias = $row->$name;
} else {
- $c_row->$alias = null;
+ $filteredRow->$alias = null;
}
}
- $result[] = $c_row;
+ } else {
+ $filteredRow = $row;
+ }
+
+ $foundStringKey |= is_string($key);
+ $result[$key] = $filteredRow;
+
+ if (count($result) === $limit) {
+ break;
}
}
// Sort the result
-
if ($query->hasOrder()) {
- usort($result, array($query, 'compare'));
- }
-
- $this->setResult($result);
- return $this;
- }
-
- protected function getLimitedResult($query)
- {
- if ($query->hasLimit()) {
- if ($query->hasOffset()) {
- $offset = $query->getOffset();
+ if ($foundStringKey) {
+ uasort($result, array($query, 'compare'));
} else {
- $offset = 0;
+ usort($result, array($query, 'compare'));
}
- return array_slice($this->result, $offset, $query->getLimit());
- } else {
- return $this->result;
+ } elseif (! $foundStringKey) {
+ $result = array_values($result);
}
+
+ return $result;
}
+ /**
+ * Return whether a query result exists
+ *
+ * @return bool
+ */
protected function hasResult()
{
return $this->result !== null;
}
- protected function setResult($result)
+ /**
+ * Set the current result
+ *
+ * @param array $result
+ *
+ * @return $this
+ */
+ protected function setResult(array $result)
{
- return $this->result = $result;
+ $this->result = $result;
+ return $this;
}
+ /**
+ * Return the result for the given query
+ *
+ * @param SimpleQuery $query
+ *
+ * @return array
+ */
protected function getResult(SimpleQuery $query)
{
if (! $this->hasResult()) {
- $this->createResult($query);
+ $this->setResult($this->createResult($query));
}
- return $this->getLimitedResult($query);
+
+ return $this->result;
}
}
diff --git a/library/Icinga/Data/Db/DbConnection.php b/library/Icinga/Data/Db/DbConnection.php
index d1817d24a..471905907 100644
--- a/library/Icinga/Data/Db/DbConnection.php
+++ b/library/Icinga/Data/Db/DbConnection.php
@@ -4,18 +4,26 @@
namespace Icinga\Data\Db;
use PDO;
+use Iterator;
use Zend_Db;
-use Icinga\Application\Benchmark;
use Icinga\Data\ConfigObject;
use Icinga\Data\Db\DbQuery;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Filter\FilterAnd;
+use Icinga\Data\Filter\FilterNot;
+use Icinga\Data\Filter\FilterOr;
+use Icinga\Data\Reducible;
use Icinga\Data\ResourceFactory;
use Icinga\Data\Selectable;
+use Icinga\Data\Updatable;
use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\ProgrammingError;
/**
* Encapsulate database connections and query creation
*/
-class DbConnection implements Selectable
+class DbConnection implements Selectable, Extensible, Updatable, Reducible
{
/**
* Connection config
@@ -72,13 +80,25 @@ class DbConnection implements Selectable
/**
* Provide a query on this connection
*
- * @return Query
+ * @return DbQuery
*/
public function select()
{
return new DbQuery($this);
}
+ /**
+ * Fetch and return all rows of the given query's result set using an iterator
+ *
+ * @param DbQuery $query
+ *
+ * @return Iterator
+ */
+ public function query(DbQuery $query)
+ {
+ return $query->getSelectQuery()->query();
+ }
+
/**
* Getter for database type
*
@@ -191,6 +211,18 @@ class DbConnection implements Selectable
return $this;
}
+ /**
+ * Count all rows of the result set
+ *
+ * @param DbQuery $query
+ *
+ * @return int
+ */
+ public function count(DbQuery $query)
+ {
+ return (int) $this->dbAdapter->fetchOne($query->getCountQuery());
+ }
+
/**
* Retrieve an array containing all rows of the result set
*
@@ -200,10 +232,7 @@ class DbConnection implements Selectable
*/
public function fetchAll(DbQuery $query)
{
- Benchmark::measure('DB is fetching All');
- $result = $this->dbAdapter->fetchAll($query->getSelectQuery());
- Benchmark::measure('DB fetch done');
- return $result;
+ return $this->dbAdapter->fetchAll($query->getSelectQuery());
}
/**
@@ -215,21 +244,17 @@ class DbConnection implements Selectable
*/
public function fetchRow(DbQuery $query)
{
- Benchmark::measure('DB is fetching row');
- $result = $this->dbAdapter->fetchRow($query->getSelectQuery());
- Benchmark::measure('DB row done');
- return $result;
+ return $this->dbAdapter->fetchRow($query->getSelectQuery());
}
/**
- * Fetch a column of all rows of the result set as an array
+ * Fetch the first column of all rows of the result set as an array
*
* @param DbQuery $query
- * @param int $columnIndex Index of the column to fetch
*
* @return array
*/
- public function fetchColumn(DbQuery $query, $columnIndex = 0)
+ public function fetchColumn(DbQuery $query)
{
return $this->dbAdapter->fetchCol($query->getSelectQuery());
}
@@ -259,4 +284,158 @@ class DbConnection implements Selectable
{
return $this->dbAdapter->fetchPairs($query->getSelectQuery());
}
+
+ /**
+ * Insert a table row with the given data
+ *
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as third parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function insert($table, array $bind, array $types = array())
+ {
+ $values = array();
+ foreach ($bind as $column => $_) {
+ $values[] = ':' . $column;
+ }
+
+ $sql = 'INSERT INTO ' . $table
+ . ' (' . join(', ', array_keys($bind)) . ') '
+ . 'VALUES (' . join(', ', $values) . ')';
+ $statement = $this->dbAdapter->prepare($sql);
+
+ foreach ($bind as $column => $value) {
+ $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
+ $statement->bindValue(':' . $column, $value, $type);
+ }
+
+ $statement->execute();
+ return $statement->rowCount();
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
+ * as fourth parameter $types to define a different type than string for a particular column.
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ * @param array $types
+ *
+ * @return int The number of affected rows
+ */
+ public function update($table, array $bind, Filter $filter = null, array $types = array())
+ {
+ $set = array();
+ foreach ($bind as $column => $_) {
+ $set[] = $column . ' = :' . $column;
+ }
+
+ $sql = 'UPDATE ' . $table
+ . ' SET ' . join(', ', $set)
+ . ($filter ? ' WHERE ' . $this->renderFilter($filter) : '');
+ $statement = $this->dbAdapter->prepare($sql);
+
+ foreach ($bind as $column => $value) {
+ $type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
+ $statement->bindValue(':' . $column, $value, $type);
+ }
+
+ $statement->execute();
+ return $statement->rowCount();
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ *
+ * @return int The number of affected rows
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ return $this->dbAdapter->delete($table, $filter ? $this->renderFilter($filter) : '');
+ }
+
+ /**
+ * Render and return the given filter as SQL-WHERE clause
+ *
+ * @param Filter $filter
+ *
+ * @return string
+ */
+ public function renderFilter(Filter $filter, $level = 0)
+ {
+ // TODO: This is supposed to supersede DbQuery::renderFilter()
+ $where = '';
+ if ($filter->isChain()) {
+ if ($filter instanceof FilterAnd) {
+ $operator = ' AND ';
+ } elseif ($filter instanceof FilterOr) {
+ $operator = ' OR ';
+ } elseif ($filter instanceof FilterNot) {
+ $operator = ' AND ';
+ $where .= ' NOT ';
+ } else {
+ throw new ProgrammingError('Cannot render filter: %s', get_class($filter));
+ }
+
+ if (! $filter->isEmpty()) {
+ $parts = array();
+ foreach ($filter->filters() as $filterPart) {
+ $part = $this->renderFilter($filterPart, $level + 1);
+ if ($part) {
+ $parts[] = $part;
+ }
+ }
+
+ if (! empty($parts)) {
+ if ($level > 0) {
+ $where .= ' (' . implode($operator, $parts) . ') ';
+ } else {
+ $where .= implode($operator, $parts);
+ }
+ }
+ } else {
+ return ''; // Explicitly return the empty string due to the FilterNot case
+ }
+ } else {
+ $where .= $this->renderFilterExpression($filter);
+ }
+
+ return $where;
+ }
+
+ /**
+ * Render and return the given filter expression
+ *
+ * @param Filter $filter
+ *
+ * @return string
+ */
+ protected function renderFilterExpression(Filter $filter)
+ {
+ $column = $filter->getColumn();
+ $sign = $filter->getSign();
+ $value = $filter->getExpression();
+
+ if (is_array($value) && $sign === '=') {
+ // TODO: Should we support this? Doesn't work for blub*
+ return $column . ' IN (' . $this->dbAdapter->quote($value) . ')';
+ } elseif ($sign === '=' && strpos($value, '*') !== false) {
+ return $column . ' LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value));
+ } elseif ($sign === '!=' && strpos($value, '*') !== false) {
+ return $column . ' NOT LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value));
+ } else {
+ return $column . ' ' . $sign . ' ' . $this->dbAdapter->quote($value);
+ }
+ }
}
diff --git a/library/Icinga/Data/Db/DbQuery.php b/library/Icinga/Data/Db/DbQuery.php
index 4fa7f132d..19849c943 100644
--- a/library/Icinga/Data/Db/DbQuery.php
+++ b/library/Icinga/Data/Db/DbQuery.php
@@ -4,13 +4,11 @@
namespace Icinga\Data\Db;
use Icinga\Data\SimpleQuery;
-use Icinga\Application\Benchmark;
use Icinga\Data\Filter\FilterChain;
-use Icinga\Data\Filter\FilterExpression;
use Icinga\Data\Filter\FilterOr;
use Icinga\Data\Filter\FilterAnd;
use Icinga\Data\Filter\FilterNot;
-use Icinga\Exception\IcingaException;
+use Icinga\Exception\QueryException;
use Zend_Db_Select;
/**
@@ -68,6 +66,7 @@ class DbQuery extends SimpleQuery
protected function init()
{
$this->db = $this->ds->getDbAdapter();
+ $this->select = $this->db->select();
parent::init();
}
@@ -77,6 +76,13 @@ class DbQuery extends SimpleQuery
return $this;
}
+ public function from($target, array $fields = null)
+ {
+ parent::from($target, $fields);
+ $this->select->from($this->target, array());
+ return $this;
+ }
+
public function where($condition, $value = null)
{
// $this->count = $this->select = null;
@@ -85,9 +91,6 @@ class DbQuery extends SimpleQuery
protected function dbSelect()
{
- if ($this->select === null) {
- $this->select = $this->db->select()->from($this->target, array());
- }
return clone $this->select;
}
@@ -153,7 +156,7 @@ class DbQuery extends SimpleQuery
$op = ' AND ';
$str .= ' NOT ';
} else {
- throw new IcingaException(
+ throw new QueryException(
'Cannot render filter: %s',
$filter
);
@@ -214,7 +217,7 @@ class DbQuery extends SimpleQuery
if (! $value) {
/*
NOTE: It's too late to throw exceptions, we might finish in __toString
- throw new IcingaException(sprintf(
+ throw new QueryException(sprintf(
'"%s" is not a valid time expression',
$value
));
@@ -298,10 +301,9 @@ class DbQuery extends SimpleQuery
public function count()
{
if ($this->count === null) {
- Benchmark::measure('DB is counting');
- $this->count = $this->db->fetchOne($this->getCountQuery());
- Benchmark::measure('DB finished count');
+ $this->count = parent::count();
}
+
return $this->count;
}
@@ -321,9 +323,7 @@ class DbQuery extends SimpleQuery
public function __clone()
{
- if ($this->select) {
- $this->select = clone $this->select;
- }
+ $this->select = clone $this->select;
}
/**
@@ -346,4 +346,137 @@ class DbQuery extends SimpleQuery
$this->group = $group;
return $this;
}
+
+ /**
+ * Return whether the given table has been joined
+ *
+ * @param string $table
+ *
+ * @return bool
+ */
+ public function hasJoinedTable($table)
+ {
+ $fromPart = $this->select->getPart(Zend_Db_Select::FROM);
+ if (isset($fromPart[$table])) {
+ return true;
+ }
+
+ foreach ($fromPart as $options) {
+ if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Add an INNER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function join($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinInner($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add an INNER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinInner($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinInner($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a LEFT OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinLeft($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinLeft($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a RIGHT OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinRight($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinRight($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a FULL OUTER JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param string $cond Join on this condition
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinFull($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinFull($name, $cond, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a CROSS JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinCross($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinCross($name, $cols, $schema);
+ return $this;
+ }
+
+ /**
+ * Add a NATURAL JOIN table and colums to the query
+ *
+ * @param array|string|Zend_Db_Expr $name The table name
+ * @param array|string $cols The columns to select from the joined table
+ * @param string $schema The database name to specify, if any
+ *
+ * @return $this
+ */
+ public function joinNatural($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
+ {
+ $this->select->joinNatural($name, $cols, $schema);
+ return $this;
+ }
}
diff --git a/library/Icinga/Data/Extensible.php b/library/Icinga/Data/Extensible.php
new file mode 100644
index 000000000..8b9f39d42
--- /dev/null
+++ b/library/Icinga/Data/Extensible.php
@@ -0,0 +1,22 @@
+compare *only*.
+ *
+ * @var array
+ */
+ protected $flippedColumns;
+
/**
* The columns you're using to sort the query result
*
@@ -92,16 +111,6 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/
protected function init() {}
- /**
- * Return a iterable for this query's result
- *
- * @return ArrayIterator
- */
- public function getIterator()
- {
- return new ArrayIterator($this->fetchAll());
- }
-
/**
* Get the data source
*
@@ -113,11 +122,75 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
}
/**
- * Choose a table and the colums you are interested in
+ * Start or rewind the iteration
+ */
+ public function rewind()
+ {
+ if ($this->iterator === null) {
+ $iterator = $this->ds->query($this);
+ if ($iterator instanceof IteratorAggregate) {
+ $this->iterator = $iterator->getIterator();
+ } else {
+ $this->iterator = $iterator;
+ }
+ }
+
+ $this->iterator->rewind();
+ Benchmark::measure('Query result iteration started');
+ }
+
+ /**
+ * Fetch and return the current row of this query's result
*
- * Query will return all available columns if none are given here
+ * @return object
+ */
+ public function current()
+ {
+ return $this->iterator->current();
+ }
+
+ /**
+ * Return whether the current row of this query's result is valid
*
- * @return $this
+ * @return bool
+ */
+ public function valid()
+ {
+ if (! $this->iterator->valid()) {
+ Benchmark::measure('Query result iteration finished');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the key for the current row of this query's result
+ *
+ * @return mixed
+ */
+ public function key()
+ {
+ return $this->iterator->key();
+ }
+
+ /**
+ * Advance to the next row of this query's result
+ */
+ public function next()
+ {
+ $this->iterator->next();
+ }
+
+ /**
+ * Choose a table and the columns you are interested in
+ *
+ * Query will return all available columns if none are given here.
+ *
+ * @param mixed $target
+ * @param array $fields
+ *
+ * @return $this
*/
public function from($target, array $fields = null)
{
@@ -226,32 +299,42 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
return $this;
}
- public function compare($a, $b, $col_num = 0)
+ /**
+ * Compare $a with $b based on this query's sort rules and column aliases
+ *
+ * @param object $a
+ * @param object $b
+ * @param int $orderIndex
+ *
+ * @return int
+ */
+ public function compare($a, $b, $orderIndex = 0)
{
- // Last column to sort reached, rows are considered being equal
- if (! array_key_exists($col_num, $this->order)) {
- return 0;
- }
- $col = $this->order[$col_num][0];
- $dir = $this->order[$col_num][1];
-// TODO: throw Exception if column is missing
- //$res = strnatcmp(strtolower($a->$col), strtolower($b->$col));
- $res = @strcmp(strtolower($a->$col), strtolower($b->$col));
- if ($res === 0) {
-// return $this->compare($a, $b, $col_num++);
-
- if (array_key_exists(++$col_num, $this->order)) {
- return $this->compare($a, $b, $col_num);
- } else {
- return 0;
- }
-
+ if (! array_key_exists($orderIndex, $this->order)) {
+ return 0; // Last column to sort reached, rows are considered being equal
}
- if ($dir === self::SORT_ASC) {
- return $res;
+ if ($this->flippedColumns === null) {
+ $this->flippedColumns = array_flip($this->columns);
+ }
+
+ $column = $this->order[$orderIndex][0];
+ if (array_key_exists($column, $this->flippedColumns)) {
+ $column = $this->flippedColumns[$column];
+ }
+
+ // TODO: throw Exception if column is missing
+ //$res = strnatcmp(strtolower($a->$column), strtolower($b->$column));
+ $result = @strcmp(strtolower($a->$column), strtolower($b->$column));
+ if ($result === 0) {
+ return $this->compare($a, $b, ++$orderIndex);
+ }
+
+ $direction = $this->order[$orderIndex][1];
+ if ($direction === self::SORT_ASC) {
+ return $result;
} else {
- return $res * -1;
+ return $result * -1;
}
}
@@ -374,7 +457,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/
public function fetchAll()
{
- return $this->ds->fetchAll($this);
+ Benchmark::measure('Fetching all results started');
+ $results = $this->ds->fetchAll($this);
+ Benchmark::measure('Fetching all results finished');
+ return $results;
}
/**
@@ -384,19 +470,23 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/
public function fetchRow()
{
- return $this->ds->fetchRow($this);
+ Benchmark::measure('Fetching one row started');
+ $row = $this->ds->fetchRow($this);
+ Benchmark::measure('Fetching one row finished');
+ return $row;
}
/**
- * Fetch a column of all rows of the result set as an array
- *
- * @param int $columnIndex Index of the column to fetch
+ * Fetch the first column of all rows of the result set as an array
*
* @return array
*/
- public function fetchColumn($columnIndex = 0)
+ public function fetchColumn()
{
- return $this->ds->fetchColumn($this, $columnIndex);
+ Benchmark::measure('Fetching one column started');
+ $values = $this->ds->fetchColumn($this);
+ Benchmark::measure('Fetching one column finished');
+ return $values;
}
/**
@@ -406,7 +496,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/
public function fetchOne()
{
- return $this->ds->fetchOne($this);
+ Benchmark::measure('Fetching one value started');
+ $value = $this->ds->fetchOne($this);
+ Benchmark::measure('Fetching one value finished');
+ return $value;
}
/**
@@ -418,17 +511,25 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/
public function fetchPairs()
{
- return $this->ds->fetchPairs($this);
+ Benchmark::measure('Fetching pairs started');
+ $pairs = $this->ds->fetchPairs($this);
+ Benchmark::measure('Fetching pairs finished');
+ return $pairs;
}
/**
- * Count all rows of the result set
+ * Count all rows of the result set, ignoring limit and offset
*
- * @return int
+ * @return int
*/
public function count()
{
- return $this->ds->count($this);
+ $query = clone $this;
+ $query->limit(0, 0);
+ Benchmark::measure('Counting all results started');
+ $count = $this->ds->count($query);
+ Benchmark::measure('Counting all results finished');
+ return $count;
}
/**
@@ -441,6 +542,7 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
public function columns(array $columns)
{
$this->columns = $columns;
+ $this->flippedColumns = null; // Reset, due to updated columns
return $this;
}
diff --git a/library/Icinga/Data/Updatable.php b/library/Icinga/Data/Updatable.php
new file mode 100644
index 000000000..ec07537cb
--- /dev/null
+++ b/library/Icinga/Data/Updatable.php
@@ -0,0 +1,24 @@
+fetchAll($query));
+ }
+
/**
* Return the number of available valid lines.
*
diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php
index 687f30e62..ba6203aab 100644
--- a/library/Icinga/Protocol/Ldap/Connection.php
+++ b/library/Icinga/Protocol/Ldap/Connection.php
@@ -3,12 +3,14 @@
namespace Icinga\Protocol\Ldap;
-use Icinga\Exception\ProgrammingError;
-use Icinga\Protocol\Ldap\Exception as LdapException;
-use Icinga\Application\Platform;
+use ArrayIterator;
use Icinga\Application\Config;
use Icinga\Application\Logger;
+use Icinga\Application\Platform;
use Icinga\Data\ConfigObject;
+use Icinga\Data\Selectable;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Protocol\Ldap\Exception as LdapException;
/**
* Backend class managing all the LDAP stuff for you.
@@ -24,7 +26,7 @@ use Icinga\Data\ConfigObject;
* ));
*
*/
-class Connection
+class Connection implements Selectable
{
const LDAP_NO_SUCH_OBJECT = 32;
const LDAP_SIZELIMIT_EXCEEDED = 4;
@@ -122,11 +124,21 @@ class Connection
return $this->root;
}
+ /**
+ * Provide a query on this connection
+ *
+ * @return Query
+ */
public function select()
{
return new Query($this);
}
+ public function query(Query $query)
+ {
+ return new ArrayIterator($this->fetchAll($query));
+ }
+
public function fetchOne($query, $fields = array())
{
$row = (array) $this->fetchRow($query, $fields);
@@ -198,13 +210,13 @@ class Connection
* @return string Returns the distinguished name, or false when the given query yields no results
* @throws LdapException When the query result is empty and contains no DN to fetch
*/
- public function fetchDN(Query $query, $fields = array())
+ public function fetchDn(Query $query, $fields = array())
{
$rows = $this->fetchAll($query, $fields);
- if (count($rows) !== 1) {
+ if (count($rows) > 1) {
throw new LdapException(
'Cannot fetch single DN for %s',
- $query->create()
+ $query
);
}
return key($rows);
@@ -235,14 +247,9 @@ class Connection
$this->connect();
$this->bind();
- $count = 0;
- $results = $this->runQuery($query);
- while (! empty($results)) {
- $count += ldap_count_entries($this->ds, $results);
- $results = $this->runQuery($query);
- }
-
- return $count;
+ // TODO: That's still not the best solution, this should probably not request any attributes
+ $res = $this->runQuery($query);
+ return count($res);
}
public function fetchAll(Query $query, $fields = array())
@@ -266,16 +273,20 @@ class Connection
* @return array The matched entries
* @throws LdapException
*/
- protected function runQuery(Query $query, $fields = array())
+ protected function runQuery(Query $query, array $fields = null)
{
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
+ if (empty($fields)) {
+ $fields = $query->getColumns();
+ }
+
$results = @ldap_search(
$this->ds,
- $query->hasBase() ? $query->getBase() : $this->root_dn,
- $query->create(),
- empty($fields) ? $query->listFields() : $fields,
+ $query->getBase() ?: $this->root_dn,
+ (string) $query,
+ array_values($fields),
0, // Attributes and values
$limit ? $offset + $limit : 0
);
@@ -286,18 +297,14 @@ class Connection
throw new LdapException(
'LDAP query "%s" (base %s) failed. Error: %s',
- $query->create(),
- $query->hasBase() ? $query->getBase() : $this->root_dn,
+ $query,
+ $query->getBase() ?: $this->root_dn,
ldap_error($this->ds)
);
} elseif (ldap_count_entries($this->ds, $results) === 0) {
return array();
}
- foreach ($query->getSortColumns() as $col) {
- ldap_sort($this->ds, $results, $col[0]);
- }
-
$count = 0;
$entries = array();
$entry = ldap_first_entry($this->ds, $results);
@@ -305,11 +312,15 @@ class Connection
$count += 1;
if ($offset === 0 || $offset < $count) {
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes(
- ldap_get_attributes($this->ds, $entry)
+ ldap_get_attributes($this->ds, $entry), array_flip($fields)
);
}
} while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry)));
+ if ($query->hasOrder()) {
+ uasort($entries, array($query, 'compare'));
+ }
+
ldap_free_result($results);
return $entries;
}
@@ -330,28 +341,29 @@ class Connection
*
* @param Query $query The query to execute
* @param array $fields The fields that will be fetched from the matches
- * @param int $page_size The maximum page size, defaults to Connection::PAGE_SIZE
+ * @param int $pageSize The maximum page size, defaults to Connection::PAGE_SIZE
*
* @return array The matched entries
* @throws LdapException
* @throws ProgrammingError When executed without available page controls (check with pageControlAvailable() )
*/
- protected function runPagedQuery(Query $query, $fields = array(), $pageSize = null)
+ protected function runPagedQuery(Query $query, array $fields = null, $pageSize = null)
{
if (! $this->pageControlAvailable($query)) {
throw new ProgrammingError('LDAP: Page control not available.');
}
+
if (! isset($pageSize)) {
$pageSize = static::PAGE_SIZE;
}
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
- $queryString = $query->create();
- $base = $query->hasBase() ? $query->getBase() : $this->root_dn;
+ $queryString = (string) $query;
+ $base = $query->getBase() ?: $this->root_dn;
if (empty($fields)) {
- $fields = $query->listFields();
+ $fields = $query->getColumns();
}
$count = 0;
@@ -362,7 +374,14 @@ class Connection
// possibillity server to return an answer in case the pagination extension is missing.
ldap_control_paged_result($this->ds, $pageSize, false, $cookie);
- $results = @ldap_search($this->ds, $base, $queryString, $fields, 0, $limit ? $offset + $limit : 0);
+ $results = @ldap_search(
+ $this->ds,
+ $base,
+ $queryString,
+ array_values($fields),
+ 0, // Attributes and values
+ $limit ? $offset + $limit : 0
+ );
if ($results === false) {
if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
break;
@@ -394,7 +413,7 @@ class Connection
$count += 1;
if ($offset === 0 || $offset < $count) {
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes(
- ldap_get_attributes($this->ds, $entry)
+ ldap_get_attributes($this->ds, $entry), array_flip($fields)
);
}
} while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry)));
@@ -420,29 +439,61 @@ class Connection
// pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by
// the server: https://www.ietf.org/rfc/rfc2696.txt
ldap_control_paged_result($this->ds, 0, false, $cookie);
- ldap_search($this->ds, $base, $queryString, $fields); // Returns no entries, due to the page size
+ ldap_search($this->ds, $base, $queryString); // Returns no entries, due to the page size
} else {
// Reset the paged search request so that subsequent requests succeed
ldap_control_paged_result($this->ds, 0);
}
+ if ($query->hasOrder()) {
+ uasort($entries, array($query, 'compare'));
+ }
+
return $entries;
}
- protected function cleanupAttributes($attrs)
+ protected function cleanupAttributes($attributes, array $requestedFields)
{
- $clean = (object) array();
- for ($i = 0; $i < $attrs['count']; $i++) {
- $attr_name = $attrs[$i];
- if ($attrs[$attr_name]['count'] === 1) {
- $clean->$attr_name = $attrs[$attr_name][0];
+ // In case the result contains attributes with a differing case than the requested fields, it is
+ // necessary to create another array to map attributes case insensitively to their requested counterparts.
+ // This does also apply the virtual alias handling. (Since an LDAP server does not handle such)
+ $loweredFieldMap = array();
+ foreach ($requestedFields as $name => $alias) {
+ $loweredFieldMap[strtolower($name)] = is_string($alias) ? $alias : $name;
+ }
+
+ $cleanedAttributes = array();
+ for ($i = 0; $i < $attributes['count']; $i++) {
+ $attribute_name = $attributes[$i];
+ if ($attributes[$attribute_name]['count'] === 1) {
+ $attribute_value = $attributes[$attribute_name][0];
} else {
- for ($j = 0; $j < $attrs[$attr_name]['count']; $j++) {
- $clean->{$attr_name}[] = $attrs[$attr_name][$j];
+ $attribute_value = array();
+ for ($j = 0; $j < $attributes[$attribute_name]['count']; $j++) {
+ $attribute_value[] = $attributes[$attribute_name][$j];
}
}
+
+ $requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)])
+ ? $loweredFieldMap[strtolower($attribute_name)]
+ : $attribute_name;
+ $cleanedAttributes[$requestedAttributeName] = $attribute_value;
}
- return $clean;
+
+ // The result may not contain all requested fields, so populate the cleaned
+ // result with the missing fields and their value being set to null
+ foreach ($requestedFields as $name => $alias) {
+ if (! is_string($alias)) {
+ $alias = $name;
+ }
+
+ if (! array_key_exists($alias, $cleanedAttributes)) {
+ $cleanedAttributes[$alias] = null;
+ Logger::debug('LDAP query result does not provide the requested field "%s"', $name);
+ }
+ }
+
+ return (object) $cleanedAttributes;
}
public function testCredentials($username, $password)
@@ -566,6 +617,10 @@ class Connection
*/
public function getCapabilities()
{
+ if ($this->capabilities === null) {
+ $this->connect(); // Populates $this->capabilities
+ }
+
return $this->capabilities;
}
@@ -590,30 +645,22 @@ class Connection
*/
protected function discoverCapabilities($ds)
{
- $query = $this->select()->from(
- '*',
- array(
- 'defaultNamingContext',
- 'namingContexts',
- 'vendorName',
- 'vendorVersion',
- 'supportedSaslMechanisms',
- 'dnsHostName',
- 'schemaNamingContext',
- 'supportedLDAPVersion', // => array(3, 2)
- 'supportedCapabilities',
- 'supportedControl',
- 'supportedExtension',
- '+'
- )
- );
- $result = @ldap_read(
- $ds,
- '',
- $query->create(),
- $query->listFields()
+ $fields = array(
+ 'defaultNamingContext',
+ 'namingContexts',
+ 'vendorName',
+ 'vendorVersion',
+ 'supportedSaslMechanisms',
+ 'dnsHostName',
+ 'schemaNamingContext',
+ 'supportedLDAPVersion', // => array(3, 2)
+ 'supportedCapabilities',
+ 'supportedControl',
+ 'supportedExtension',
+ '+'
);
+ $result = @ldap_read($ds, '', (string) $this->select()->from('*', $fields), $fields);
if (! $result) {
throw new LdapException(
'Capability query failed (%s:%d): %s. Check if hostname and port of the'
@@ -623,6 +670,7 @@ class Connection
ldap_error($ds)
);
}
+
$entry = ldap_first_entry($ds, $result);
if ($entry === false) {
throw new LdapException(
@@ -633,9 +681,7 @@ class Connection
);
}
- $ldapAttributes = ldap_get_attributes($ds, $entry);
- $result = $this->cleanupAttributes($ldapAttributes);
- return new Capability($result);
+ return new Capability($this->cleanupAttributes(ldap_get_attributes($ds, $entry), array_flip($fields)));
}
/**
diff --git a/library/Icinga/Protocol/Ldap/Query.php b/library/Icinga/Protocol/Ldap/Query.php
index 004c2e43e..b54f8b15e 100644
--- a/library/Icinga/Protocol/Ldap/Query.php
+++ b/library/Icinga/Protocol/Ldap/Query.php
@@ -3,151 +3,154 @@
namespace Icinga\Protocol\Ldap;
+use Icinga\Data\SimpleQuery;
+use Icinga\Data\Filter\Filter;
+use Icinga\Exception\NotImplementedError;
+
/**
- * Search class
- *
- * @package Icinga\Protocol\Ldap
+ * LDAP query class
*/
-/**
- * Search abstraction class
- *
- * Usage example:
- *
- *
- * $connection->select()->from('user')->where('sAMAccountName = ?', 'icinga');
- *
- *
- * @copyright Copyright (c) 2013 Icinga-Web Team
- * @author Icinga-Web Team
- * @package Icinga\Protocol\Ldap
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
- */
-class Query
+class Query extends SimpleQuery
{
- protected $connection;
- protected $filters = array();
- protected $fields = array();
- protected $limit_count = 0;
- protected $limit_offset = 0;
- protected $sort_columns = array();
- protected $count;
- protected $base;
- protected $usePagedResults = true;
+ /**
+ * This query's filters
+ *
+ * Currently just a basic key/value pair based array. Can be removed once Icinga\Data\Filter is supported.
+ *
+ * @var array
+ */
+ protected $filters;
/**
- * Constructor
+ * The base dn being used for this query
*
- * @param Connection LDAP Connection object
- * @return void
+ * @var string
*/
- public function __construct(Connection $connection)
+ protected $base;
+
+ /**
+ * Whether this query is permitted to utilize paged results
+ *
+ * @var bool
+ */
+ protected $usePagedResults;
+
+ /**
+ * Initialize this query
+ */
+ protected function init()
{
- $this->connection = $connection;
+ $this->filters = array();
+ $this->usePagedResults = true;
}
+ /**
+ * Set the base dn to be used for this query
+ *
+ * @param string $base
+ *
+ * @return $this
+ */
public function setBase($base)
{
$this->base = $base;
return $this;
}
- public function hasBase()
- {
- return $this->base !== null;
- }
-
+ /**
+ * Return the base dn being used for this query
+ *
+ * @return string
+ */
public function getBase()
{
return $this->base;
}
+ /**
+ * Set whether this query is permitted to utilize paged results
+ *
+ * @param bool $state
+ *
+ * @return $this
+ */
public function setUsePagedResults($state = true)
{
$this->usePagedResults = (bool) $state;
return $this;
}
+ /**
+ * Return whether this query is permitted to utilize paged results
+ *
+ * @return bool
+ */
public function getUsePagedResults()
{
return $this->usePagedResults;
}
/**
- * Count result set, ignoring limits
+ * Choose an objectClass and the columns you are interested in
*
- * @return int
+ * {@inheritdoc} This creates an objectClass filter.
*/
- public function count()
+ public function from($target, array $fields = null)
{
- if ($this->count === null) {
- $this->count = $this->connection->count($this);
- }
- return $this->count;
+ $this->filters['objectClass'] = $target;
+ return parent::from($target, $fields);
}
/**
- * Count result set, ignoring limits
+ * Add a new filter to the query
*
- * @return int
+ * @param string $condition Column to search in
+ * @param mixed $value Value to look for (asterisk wildcards are allowed)
+ *
+ * @return $this
*/
- public function limit($count = null, $offset = null)
+ public function where($condition, $value = null)
{
- if (! preg_match('~^\d+~', $count . $offset)) {
- throw new Exception(
- 'Got invalid limit: %s, %s',
- $count,
- $offset
- );
+ // TODO: Adjust this once support for Icinga\Data\Filter is available
+ if ($condition instanceof Expression) {
+ $this->filters[] = $condition;
+ } else {
+ $this->filters[$condition] = $value;
}
- $this->limit_count = (int) $count;
- $this->limit_offset = (int) $offset;
+
return $this;
}
- /**
- * Whether a limit has been set
- *
- * @return boolean
- */
- public function hasLimit()
+ public function getFilter()
{
- return $this->limit_count > 0;
+ throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
- /**
- * Whether an offset (limit) has been set
- *
- * @return boolean
- */
- public function hasOffset()
+ public function addFilter(Filter $filter)
{
- return $this->limit_offset > 0;
+ // TODO: This should be considered a quick fix only.
+ // Drop this entirely once support for Icinga\Data\Filter is available
+ if ($filter->isExpression()) {
+ $this->where($filter->getColumn(), $filter->getExpression());
+ } elseif ($filter->isChain()) {
+ foreach ($filter->filters() as $chainOrExpression) {
+ $this->addFilter($chainOrExpression);
+ }
+ }
}
- /**
- * Retrieve result limit
- *
- * @return int
- */
- public function getLimit()
+ public function setFilter(Filter $filter)
{
- return $this->limit_count;
- }
-
- /**
- * Retrieve result offset
- *
- * @return int
- */
- public function getOffset()
- {
- return $this->limit_offset;
+ throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
/**
* Fetch result as tree
*
- * @return Node
+ * @return Root
+ *
+ * @todo This is untested waste, not being used anywhere and ignores the query's order and base dn.
+ * Evaluate whether it's reasonable to properly implement and test it.
*/
public function fetchTree()
{
@@ -179,132 +182,32 @@ class Query
}
/**
- * Fetch result as an array of objects
+ * Fetch the distinguished name of the first result
*
- * @return array
+ * @return string|false The distinguished name or false in case it's not possible to fetch a result
+ *
+ * @throws Exception In case the query returns multiple results
+ * (i.e. it's not possible to fetch a unique DN)
*/
- public function fetchAll()
+ public function fetchDn()
{
- return $this->connection->fetchAll($this);
+ return $this->ds->fetchDn($this);
}
/**
- * Fetch first result row
+ * Return the LDAP filter to be applied on this query
*
- * @return object
+ * @return string
+ *
+ * @throws Exception In case the objectClass filter does not exist
*/
- public function fetchRow()
+ protected function renderFilter()
{
- return $this->connection->fetchRow($this);
- }
-
- /**
- * Fetch first column value from first result row
- *
- * @return mixed
- */
- public function fetchOne()
- {
- return $this->connection->fetchOne($this);
- }
-
- /**
- * Fetch a key/value list, first column is key, second is value
- *
- * @return array
- */
- public function fetchPairs()
- {
- // STILL TODO!!
- return $this->connection->fetchPairs($this);
- }
-
- /**
- * Where to select (which fields) from
- *
- * This creates an objectClass filter
- *
- * @return Query
- */
- public function from($objectClass, $fields = array())
- {
- $this->filters['objectClass'] = $objectClass;
- $this->fields = $fields;
- return $this;
- }
-
- /**
- * Add a new filter to the query
- *
- * @param string Column to search in
- * @param string Filter text (asterisks are allowed)
- * @return Query
- */
- public function where($key, $val)
- {
- $this->filters[$key] = $val;
- return $this;
- }
-
- /**
- * Sort by given column
- *
- * TODO: Sort direction is not implemented yet
- *
- * @param string Order column
- * @param string Order direction
- * @return Query
- */
- public function order($column, $direction = 'ASC')
- {
- $this->sort_columns[] = array($column, $direction);
- return $this;
- }
-
- /**
- * Retrieve a list of the desired fields
- *
- * @return array
- */
- public function listFields()
- {
- return $this->fields;
- }
-
- /**
- * Retrieve a list containing current sort columns
- *
- * @return array
- */
- public function getSortColumns()
- {
- return $this->sort_columns;
- }
-
- /**
- * Add a filter expression to this query
- *
- * @param Expression $expression
- *
- * @return Query
- */
- public function addFilter(Expression $expression)
- {
- $this->filters[] = $expression;
- return $this;
- }
-
- /**
- * Returns the LDAP filter that will be applied
- *
- * @string
- */
- public function create()
- {
- $parts = array();
- if (! isset($this->filters['objectClass']) || $this->filters['objectClass'] === null) {
+ if (! isset($this->filters['objectClass'])) {
throw new Exception('Object class is mandatory');
}
+
+ $parts = array();
foreach ($this->filters as $key => $value) {
if ($value instanceof Expression) {
$parts[] = (string) $value;
@@ -316,6 +219,7 @@ class Query
);
}
}
+
if (count($parts) > 1) {
return '(&(' . implode(')(', $parts) . '))';
} else {
@@ -323,17 +227,13 @@ class Query
}
}
+ /**
+ * Return the LDAP filter to be applied on this query
+ *
+ * @return string
+ */
public function __toString()
{
- return $this->create();
- }
-
- /**
- * Descructor
- */
- public function __destruct()
- {
- // To be on the safe side:
- unset($this->connection);
+ return $this->renderFilter();
}
}
diff --git a/library/Icinga/Repository/DbRepository.php b/library/Icinga/Repository/DbRepository.php
new file mode 100644
index 000000000..93f9ceaeb
--- /dev/null
+++ b/library/Icinga/Repository/DbRepository.php
@@ -0,0 +1,755 @@
+
+ * Support for table aliases
+ * Automatic table prefix handling
+ * Insert, update and delete capabilities
+ * Differentiation between statement and query columns
+ * Capability to join additional tables depending on the columns being selected or used in a filter
+ *
+ */
+abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * The datasource being used
+ *
+ * @var DbConnection
+ */
+ protected $ds;
+
+ /**
+ * The table aliases being applied
+ *
+ * This must be initialized by repositories which are going to make use of table aliases. Every table for which
+ * aliased columns are provided must be defined in this array using its name as key and the alias being used as
+ * value. Failure to do so will result in invalid queries.
+ *
+ * @var array
+ */
+ protected $tableAliases;
+
+ /**
+ * The statement columns being provided
+ *
+ * This may be initialized by repositories which are going to make use of table aliases. It allows to provide
+ * alias-less column names to be used for a statement. The array needs to be in the following format:
+ *
+ * array(
+ * 'table_name' => array(
+ * 'column1',
+ * 'alias1' => 'column2',
+ * 'alias2' => 'column3'
+ * )
+ * )
+ *
+ *
+ * @var array
+ */
+ protected $statementColumns;
+
+ /**
+ * An array to map table names to statement columns/aliases
+ *
+ * @var array
+ */
+ protected $statementTableMap;
+
+ /**
+ * A flattened array to map statement columns to aliases
+ *
+ * @var array
+ */
+ protected $statementColumnMap;
+
+ /**
+ * List of columns where the COLLATE SQL-instruction has been removed
+ *
+ * This list is being populated in case of a PostgreSQL backend only,
+ * to ensure case-insensitive string comparison in WHERE clauses.
+ *
+ * @var array
+ */
+ protected $columnsWithoutCollation;
+
+ /**
+ * Create a new DB repository object
+ *
+ * In case $this->queryColumns has already been initialized, this initializes
+ * $this->columnsWithoutCollation in case of a PostgreSQL connection.
+ *
+ * @param DbConnection $ds The datasource to use
+ */
+ public function __construct(DbConnection $ds)
+ {
+ parent::__construct($ds);
+
+ $this->columnsWithoutCollation = array();
+ if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ /**
+ * Return the query columns being provided
+ *
+ * Initializes $this->columnsWithoutCollation in case of a PostgreSQL connection.
+ *
+ * @return array
+ */
+ public function getQueryColumns()
+ {
+ if ($this->queryColumns === null) {
+ $this->queryColumns = parent::getQueryColumns();
+ if ($this->ds->getDbType() === 'pgsql') {
+ $this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
+ }
+ }
+
+ return $this->queryColumns;
+ }
+
+ /**
+ * Return the table aliases to be applied
+ *
+ * Calls $this->initializeTableAliases() in case $this->tableAliases is null.
+ *
+ * @return array
+ */
+ public function getTableAliases()
+ {
+ if ($this->tableAliases === null) {
+ $this->tableAliases = $this->initializeTableAliases();
+ }
+
+ return $this->tableAliases;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the table aliases lazily
+ *
+ * @return array
+ */
+ protected function initializeTableAliases()
+ {
+ return array();
+ }
+
+ /**
+ * Remove each COLLATE SQL-instruction from all given query columns
+ *
+ * @param array $queryColumns
+ *
+ * @return array $queryColumns, the updated version
+ */
+ protected function removeCollateInstruction($queryColumns)
+ {
+ foreach ($queryColumns as & $columns) {
+ foreach ($columns as & $column) {
+ $column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
+ if ($count > 0) {
+ $this->columnsWithoutCollation[] = $column;
+ }
+ }
+ }
+
+ return $queryColumns;
+ }
+
+ /**
+ * Return the given table with the datasource's prefix being prepended
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function prependTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === false) {
+ $tableName = $prefix . $tableName;
+ }
+ }
+ } elseif (is_string($table)) {
+ $table = (strpos($table, $prefix) === false ? $prefix : '') . $table;
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Remove the datasource's prefix from the given table name and return the remaining part
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function removeTablePrefix($table)
+ {
+ $prefix = $this->ds->getTablePrefix();
+ if (! $prefix) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ foreach ($table as & $tableName) {
+ if (strpos($tableName, $prefix) === 0) {
+ $tableName = str_replace($prefix, '', $tableName);
+ }
+ }
+ } elseif (is_string($table)) {
+ if (strpos($table, $prefix) === 0) {
+ $table = str_replace($prefix, '', $table);
+ }
+ } else {
+ throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
+ }
+
+ return $table;
+ }
+
+ /**
+ * Return the given table with its alias being applied
+ *
+ * @param array|string $table
+ *
+ * @return array|string
+ */
+ protected function applyTableAlias($table)
+ {
+ $tableAliases = $this->getTableAliases();
+ if (is_array($table) || !isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
+ return $table;
+ }
+
+ return array($tableAliases[$nonPrefixedTable] => $table);
+ }
+
+ /**
+ * Return the given table with its alias being cleared
+ *
+ * @param array|string $table
+ *
+ * @return string
+ *
+ * @throws IcingaException In case $table is not of a supported type
+ */
+ protected function clearTableAlias($table)
+ {
+ if (is_string($table)) {
+ return $table;
+ }
+
+ if (is_array($table)) {
+ return reset($table);
+ }
+
+ throw new IcingaException('Table alias handling for type "%s" is not supported', type($table));
+ }
+
+ /**
+ * Insert a table row with the given data
+ *
+ * @param string $table
+ * @param array $bind
+ */
+ public function insert($table, array $bind)
+ {
+ $this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind));
+ }
+
+ /**
+ * Update table rows with the given data, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param array $bind
+ * @param Filter $filter
+ */
+ public function update($table, array $bind, Filter $filter = null)
+ {
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ $this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter);
+ }
+
+ /**
+ * Delete table rows, optionally limited by using a filter
+ *
+ * @param string $table
+ * @param Filter $filter
+ */
+ public function delete($table, Filter $filter = null)
+ {
+ if ($filter) {
+ $filter = $this->requireFilter($table, $filter);
+ }
+
+ $this->ds->delete($this->prependTablePrefix($table), $filter);
+ }
+
+ /**
+ * Return the statement columns being provided
+ *
+ * Calls $this->initializeStatementColumns() in case $this->statementColumns is null.
+ *
+ * @return array
+ */
+ public function getStatementColumns()
+ {
+ if ($this->statementColumns === null) {
+ $this->statementColumns = $this->initializeStatementColumns();
+ }
+
+ return $this->statementColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the statement columns lazily
+ *
+ * @return array
+ */
+ protected function initializeStatementColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return an array to map table names to statement columns/aliases
+ *
+ * @return array
+ */
+ protected function getStatementTableMap()
+ {
+ if ($this->statementTableMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementTableMap;
+ }
+
+ /**
+ * Return a flattened array to map statement columns to aliases
+ *
+ * @return array
+ */
+ protected function getStatementColumnMap()
+ {
+ if ($this->statementColumnMap === null) {
+ $this->initializeStatementMaps();
+ }
+
+ return $this->statementColumnMap;
+ }
+
+ /**
+ * Initialize $this->statementTableMap and $this->statementColumnMap
+ */
+ protected function initializeStatementMaps()
+ {
+ $this->statementTableMap = array();
+ $this->statementColumnMap = array();
+ foreach ($this->getStatementColumns() as $table => $columns) {
+ foreach ($columns as $alias => $column) {
+ $key = is_string($alias) ? $alias : $column;
+ if (array_key_exists($key, $this->statementTableMap)) {
+ if ($this->statementTableMap[$key] !== null) {
+ $existingTable = $this->statementTableMap[$key];
+ $existingColumn = $this->statementColumnMap[$key];
+ $this->statementTableMap[$existingTable . '.' . $key] = $existingTable;
+ $this->statementColumnMap[$existingTable . '.' . $key] = $existingColumn;
+ $this->statementTableMap[$key] = null;
+ $this->statementColumnMap[$key] = null;
+ }
+
+ $this->statementTableMap[$table . '.' . $key] = $table;
+ $this->statementColumnMap[$table . '.' . $key] = $column;
+ } else {
+ $this->statementTableMap[$key] = $table;
+ $this->statementColumnMap[$key] = $column;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return whether this repository is capable of converting values
+ *
+ * This does not check whether any conversion for the given table is available, as it may be possible
+ * that columns from another table where joined in which would otherwise not being converted.
+ *
+ * @param array|string $table
+ *
+ * @return bool
+ */
+ public function providesValueConversion($_)
+ {
+ $conversionRules = $this->getConversionRules();
+ return !empty($conversionRules);
+ }
+
+ /**
+ * Return the name of the conversion method for the given alias or column name and context
+ *
+ * @param array|string $table The datasource's table
+ * @param string $name The alias or column name for which to return a conversion method
+ * @param string $context The context of the conversion: persist or retrieve
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case a conversion rule is found but not any conversion method
+ */
+ protected function getConverter($table, $name, $context)
+ {
+ if (
+ $this->validateQueryColumnAssociation($table, $name)
+ || $this->validateStatementColumnAssociation($table, $name)
+ ) {
+ $table = $this->removeTablePrefix($this->clearTableAlias($table));
+ } else {
+ $table = $this->findTableName($name);
+ if (! $table) {
+ throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?');
+ }
+ }
+
+ return parent::getConverter($table, $name, $context);
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * This will prepend the datasource's table prefix and will apply the table's alias, if any.
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return array|string
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $statementColumns = $this->getStatementColumns();
+ if (! isset($statementColumns[$table])) {
+ $table = parent::requireTable($table);
+ }
+
+ return $this->prependTablePrefix($this->applyTableAlias($table));
+ }
+
+ /**
+ * Recurse the given filter, require each column for the given table and convert all values
+ *
+ * In case of a PostgreSQL connection, this applies LOWER() on the column and strtolower()
+ * on the value if a COLLATE SQL-instruction is part of the resolved column.
+ *
+ * @param string $table The table being filtered
+ * @param Filter $filter The filter to recurse
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (Directly passed through to $this->requireFilterColumn)
+ * @param bool $clone Whether to clone $filter first
+ *
+ * @return Filter The udpated filter
+ */
+ public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
+ {
+ $filter = parent::requireFilter($table, $filter, $query, $clone);
+
+ if ($filter->isExpression()) {
+ $column = $filter->getColumn();
+ if (in_array($column, $this->columnsWithoutCollation) && strpos($column, 'LOWER') !== 0) {
+ $filter->setColumn('LOWER(' . $column . ')');
+ $expression = $filter->getExpression();
+ if (is_array($expression)) {
+ $filter->setExpression(array_map('strtolower', $expression));
+ } else {
+ $filter->setExpression(strtolower($expression));
+ }
+ }
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Return this repository's query columns of the given table mapped to their respective aliases
+ *
+ * @param array|string $table
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $table does not exist
+ */
+ public function requireAllQueryColumns($table)
+ {
+ return parent::requireAllQueryColumns($this->removeTablePrefix($this->clearTableAlias($table)));
+ }
+
+ /**
+ * Return the query column name for the given alias or null in case the alias does not exist
+ *
+ * @param array|string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveQueryColumnAlias($table, $alias)
+ {
+ return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias);
+ }
+
+ /**
+ * Return whether the given query column name or alias is available in the given table
+ *
+ * @param array|string $table
+ * @param string $column
+ *
+ * @return bool
+ */
+ public function validateQueryColumnAssociation($table, $column)
+ {
+ return parent::validateQueryColumnAssociation(
+ $this->removeTablePrefix($this->clearTableAlias($table)),
+ $column
+ );
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given no join will be attempted
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid query column
+ */
+ public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if ($query === null || $this->validateQueryColumnAssociation($table, $name)) {
+ return parent::requireQueryColumn($table, $name, $query);
+ }
+
+ return $this->joinColumn($name, $table, $query);
+ }
+
+ /**
+ * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
+ *
+ * Attempts to join the given column from a different table if its association to the given table cannot be
+ * verified.
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context,
+ * if not given the column is considered being used for a statement filter
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid filter column
+ */
+ public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if ($query === null) {
+ return $this->requireStatementColumn($table, $name);
+ }
+
+ if ($this->validateQueryColumnAssociation($table, $name)) {
+ return parent::requireFilterColumn($table, $name, $query);
+ }
+
+ return $this->joinColumn($name, $table, $query);
+ }
+
+ /**
+ * Return the statement column name for the given alias or null in case the alias does not exist
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveStatementColumnAlias($table, $alias)
+ {
+ $statementColumnMap = $this->getStatementColumnMap();
+ if (isset($statementColumnMap[$alias])) {
+ return $statementColumnMap[$alias];
+ }
+
+ $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
+ if (isset($statementColumnMap[$prefixedAlias])) {
+ return $statementColumnMap[$prefixedAlias];
+ }
+ }
+
+ /**
+ * Return whether the given alias or statement column name is available in the given table
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function validateStatementColumnAssociation($table, $alias)
+ {
+ $statementTableMap = $this->getStatementTableMap();
+ if (isset($statementTableMap[$alias])) {
+ return $statementTableMap[$alias] === $this->removeTablePrefix($table);
+ }
+
+ $prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
+ return isset($statementTableMap[$prefixedAlias]);
+ }
+
+ /**
+ * Return whether the given column name or alias of the given table is a valid statement column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($table, $name)
+ {
+ if (
+ $this->resolveStatementColumnAlias($table, $name) === null
+ || !$this->validateStatementColumnAssociation($table, $name)
+ ) {
+ return parent::hasStatementColumn($table, $name);
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $table The table for which to require the column
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($table, $name)
+ {
+ if (($column = $this->resolveStatementColumnAlias($table, $name)) === null) {
+ return parent::requireStatementColumn($table, $name);
+ }
+
+ if (! $this->validateStatementColumnAssociation($table, $name)) {
+ throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Join alias or column $name into $table using $query
+ *
+ * Attempts to find a valid table for the given alias or column name and a method labelled join
+ * to process the actual join logic. If neither of those is found, ProgrammingError will be thrown.
+ * The method is called with the same parameters but in reversed order.
+ *
+ * @param string $name The alias or column name to join into $target
+ * @param array|string $target The table to join $name into
+ * @param RepositoryQUery $query The query to apply the JOIN-clause on
+ *
+ * @return string The resolved alias or $name
+ *
+ * @throws ProgrammingError In case no valid table or join-method is found
+ */
+ public function joinColumn($name, $target, RepositoryQuery $query)
+ {
+ $tableName = $this->findTableName($name);
+ if (! $tableName) {
+ throw new ProgrammingError(
+ 'Unable to find a valid table for column "%s" to join into "%s"',
+ $name,
+ $this->removeTablePrefix($this->clearTableAlias($target))
+ );
+ }
+
+ $column = $this->resolveQueryColumnAlias($tableName, $name);
+
+ $prefixedTableName = $this->prependTablePrefix($tableName);
+ if ($query->getQuery()->hasJoinedTable($prefixedTableName)) {
+ return $column;
+ }
+
+ $joinMethod = 'join' . String::cname($tableName);
+ if (! method_exists($this, $joinMethod)) {
+ throw new ProgrammingError(
+ 'Unable to join table "%s" into "%s". Method "%s" not found',
+ $tableName,
+ $this->removeTablePrefix($this->clearTableAlias($target)),
+ $joinMethod
+ );
+ }
+
+ $this->$joinMethod($query, $target, $name);
+ return $column;
+ }
+
+ /**
+ * Return the table name for the given alias or column name
+ *
+ * @param string $column
+ *
+ * @return string|null null in case no table is found
+ */
+ protected function findTableName($column)
+ {
+ $aliasTableMap = $this->getAliasTableMap();
+ if (isset($aliasTableMap[$column])) {
+ return $aliasTableMap[$column];
+ }
+
+ // TODO(jom): Elaborate whether it makes sense to throw ProgrammingError
+ // instead (duplicate aliases in different tables?)
+ foreach ($aliasTableMap as $alias => $table) {
+ if (strpos($alias, '.') !== false) {
+ list($_, $alias) = explode('.', $column, 2);
+ if ($alias === $column) {
+ return $table;
+ }
+ }
+ }
+ }
+}
diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php
new file mode 100644
index 000000000..3c73464e5
--- /dev/null
+++ b/library/Icinga/Repository/IniRepository.php
@@ -0,0 +1,186 @@
+
+ * Insert, update and delete capabilities
+ *
+ */
+abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * The datasource being used
+ *
+ * @var Config
+ */
+ protected $ds;
+
+ /**
+ * Create a new INI repository object
+ *
+ * @param Config $ds The data source to use
+ *
+ * @throws ProgrammingError In case the given data source does not provide a valid key column
+ */
+ public function __construct(Config $ds)
+ {
+ parent::__construct($ds); // First! Due to init().
+
+ if (! $ds->getConfigObject()->getKeyColumn()) {
+ throw new ProgrammingError('INI repositories require their data source to provide a valid key column');
+ }
+ }
+
+ /**
+ * Insert the given data for the given target
+ *
+ * $data must provide a proper value for the data source's key column.
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function insert($target, array $data)
+ {
+ $newData = $this->requireStatementColumns($target, $data);
+ $section = $this->extractSectionName($newData);
+
+ if ($this->ds->hasSection($section)) {
+ throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
+ }
+
+ $this->ds->setSection($section, $newData);
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Update the target with the given data and optionally limit the affected entries by using a filter
+ *
+ * @param string $target
+ * @param array $data
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function update($target, array $data, Filter $filter = null)
+ {
+ $newData = $this->requireStatementColumns($target, $data);
+ $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
+ if ($filter === null && isset($newData[$keyColumn])) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ if ($filter !== null) {
+ $filter = $this->requireFilter($target, $filter);
+ }
+
+ $newSection = null;
+ foreach (iterator_to_array($this->ds) as $section => $config) {
+ if ($filter !== null && !$filter->matches($config)) {
+ continue;
+ }
+
+ if ($newSection !== null) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ foreach ($newData as $column => $value) {
+ if ($column === $keyColumn) {
+ $newSection = $value;
+ } else {
+ $config->$column = $value;
+ }
+ }
+
+ if ($newSection) {
+ if ($this->ds->hasSection($newSection)) {
+ throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection);
+ }
+
+ $this->ds->removeSection($section)->setSection($newSection, $config);
+ } else {
+ $this->ds->setSection($section, $config);
+ }
+ }
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Delete entries in the given target, optionally limiting the affected entries by using a filter
+ *
+ * @param string $target
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function delete($target, Filter $filter = null)
+ {
+ if ($filter !== null) {
+ $filter = $this->requireFilter($target, $filter);
+ }
+
+ foreach (iterator_to_array($this->ds) as $section => $config) {
+ if ($filter === null || $filter->matches($config)) {
+ $this->ds->removeSection($section);
+ }
+ }
+
+ try {
+ $this->ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Extract and return the section name off of the given $data
+ *
+ * @param array $data
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no valid section name is available
+ */
+ protected function extractSectionName(array & $data)
+ {
+ $keyColumn = $this->ds->getConfigObject()->getKeyColumn();
+ if (! isset($data[$keyColumn])) {
+ throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn);
+ }
+
+ $section = $data[$keyColumn];
+ unset($data[$keyColumn]);
+ return $section;
+ }
+}
diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php
new file mode 100644
index 000000000..803f4958f
--- /dev/null
+++ b/library/Icinga/Repository/Repository.php
@@ -0,0 +1,861 @@
+
+ * Concrete implementations need to initialize Repository::$queryColumns
+ * The datasource passed to a repository must implement the Selectable interface
+ * The datasource must yield an instance of Queryable when its select() method is called
+ *
+ */
+abstract class Repository implements Selectable
+{
+ /**
+ * The format to use when converting values of type date_time
+ */
+ const DATETIME_FORMAT = 'd/m/Y g:i A';
+
+ /**
+ * The name of this repository
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * The datasource being used
+ *
+ * @var Selectable
+ */
+ protected $ds;
+
+ /**
+ * The base table name this repository is responsible for
+ *
+ * This will be automatically set to the first key of $queryColumns if not explicitly set.
+ *
+ * @var string
+ */
+ protected $baseTable;
+
+ /**
+ * The query columns being provided
+ *
+ * This must be initialized by concrete repository implementations, in the following format
+ *
+ * array(
+ * 'baseTable' => array(
+ * 'column1',
+ * 'alias1' => 'column2',
+ * 'alias2' => 'column3'
+ * )
+ * )
+ *
+ *
+ * @var array
+ */
+ protected $queryColumns;
+
+ /**
+ * The columns (or aliases) which are not permitted to be queried. (by design)
+ *
+ * @var array An array of strings
+ */
+ protected $filterColumns;
+
+ /**
+ * The default sort rules to be applied on a query
+ *
+ * This may be initialized by concrete repository implementations, in the following format
+ *
+ * array(
+ * 'alias_or_column_name' => array(
+ * 'order' => 'asc'
+ * ),
+ * 'alias_or_column_name' => array(
+ * 'columns' => array(
+ * 'once_more_the_alias_or_column_name_as_in_the_parent_key',
+ * 'an_additional_alias_or_column_name_with_a_specific_direction asc'
+ * ),
+ * 'order' => 'desc'
+ * ),
+ * 'alias_or_column_name' => array(
+ * 'columns' => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column')
+ * // Ascendant sort by default
+ * )
+ * )
+ *
+ * Note that it's mandatory to supply the alias name in case there is one.
+ *
+ * @var array
+ */
+ protected $sortRules;
+
+ /**
+ * The value conversion rules to apply on a query or statement
+ *
+ * This may be initialized by concrete repository implementations and describes for which aliases or column
+ * names what type of conversion is available. For entries, where the key is the alias/column and the value
+ * is the type identifier, the repository attempts to find a conversion method for the alias/column first and,
+ * if none is found, then for the type. If an entry only provides a value, which is the alias/column, the
+ * repository only attempts to find a conversion method for the alias/column. The name of a conversion method
+ * is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and
+ * groupname will be translated to retrieveGroupname)
+ *
+ * @var array
+ */
+ protected $conversionRules;
+
+ /**
+ * An array to map table names to aliases
+ *
+ * @var array
+ */
+ protected $aliasTableMap;
+
+ /**
+ * A flattened array to map query columns to aliases
+ *
+ * @var array
+ */
+ protected $aliasColumnMap;
+
+ /**
+ * Create a new repository object
+ *
+ * @param Selectable $ds The datasource to use
+ */
+ public function __construct(Selectable $ds)
+ {
+ $this->ds = $ds;
+ $this->aliasTableMap = array();
+ $this->aliasColumnMap = array();
+
+ $this->init();
+ }
+
+ /**
+ * Initialize this repository
+ *
+ * Supposed to be overwritten by concrete repository implementations.
+ */
+ protected function init()
+ {
+
+ }
+
+ /**
+ * 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 ?: __CLASS__;
+ }
+
+ /**
+ * Return the datasource being used
+ *
+ * @return Selectable
+ */
+ public function getDataSource()
+ {
+ return $this->ds;
+ }
+
+ /**
+ * Return the base table name this repository is responsible for
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no base table name has been set and
+ * $this->queryColumns does not provide one either
+ */
+ public function getBaseTable()
+ {
+ if ($this->baseTable === null) {
+ $queryColumns = $this->getQueryColumns();
+ reset($queryColumns);
+ $this->baseTable = key($queryColumns);
+ if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) {
+ throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable);
+ }
+ }
+
+ return $this->baseTable;
+ }
+
+ /**
+ * Return the query columns being provided
+ *
+ * Calls $this->initializeQueryColumns() in case $this->queryColumns is null.
+ *
+ * @return array
+ */
+ public function getQueryColumns()
+ {
+ if ($this->queryColumns === null) {
+ $this->queryColumns = $this->initializeQueryColumns();
+ }
+
+ return $this->queryColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the query columns lazily
+ *
+ * @return array
+ */
+ protected function initializeQueryColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return the columns (or aliases) which are not permitted to be queried
+ *
+ * Calls $this->initializeFilterColumns() in case $this->filterColumns is null.
+ *
+ * @return array
+ */
+ public function getFilterColumns()
+ {
+ if ($this->filterColumns === null) {
+ $this->filterColumns = $this->initializeFilterColumns();
+ }
+
+ return $this->filterColumns;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the filter columns lazily
+ *
+ * @return array
+ */
+ protected function initializeFilterColumns()
+ {
+ return array();
+ }
+
+ /**
+ * Return the default sort rules to be applied on a query
+ *
+ * Calls $this->initializeSortRules() in case $this->sortRules is null.
+ *
+ * @return array
+ */
+ public function getSortRules()
+ {
+ if ($this->sortRules === null) {
+ $this->sortRules = $this->initializeSortRules();
+ }
+
+ return $this->sortRules;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the sort rules lazily
+ *
+ * @return array
+ */
+ protected function initializeSortRules()
+ {
+ return array();
+ }
+
+ /**
+ * Return the value conversion rules to apply on a query
+ *
+ * Calls $this->initializeConversionRules() in case $this->conversionRules is null.
+ *
+ * @return array
+ */
+ public function getConversionRules()
+ {
+ if ($this->conversionRules === null) {
+ $this->conversionRules = $this->initializeConversionRules();
+ }
+
+ return $this->conversionRules;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily
+ *
+ * @return array
+ */
+ protected function initializeConversionRules()
+ {
+ return array();
+ }
+
+ /**
+ * Return an array to map table names to aliases
+ *
+ * @return array
+ */
+ protected function getAliasTableMap()
+ {
+ if (empty($this->aliasTableMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->aliasTableMap;
+ }
+
+ /**
+ * Return a flattened array to map query columns to aliases
+ *
+ * @return array
+ */
+ protected function getAliasColumnMap()
+ {
+ if (empty($this->aliasColumnMap)) {
+ $this->initializeAliasMaps();
+ }
+
+ return $this->aliasColumnMap;
+ }
+
+ /**
+ * Initialize $this->aliasTableMap and $this->aliasColumnMap
+ *
+ * @throws ProgrammingError In case $this->queryColumns does not provide any column information
+ */
+ protected function initializeAliasMaps()
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (empty($queryColumns)) {
+ throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
+ }
+
+ foreach ($queryColumns as $table => $columns) {
+ foreach ($columns as $alias => $column) {
+ if (! is_string($alias)) {
+ $key = $column;
+ } else {
+ $key = $alias;
+ $column = preg_replace('~\n\s*~', ' ', $column);
+ }
+
+ if (array_key_exists($key, $this->aliasTableMap)) {
+ if ($this->aliasTableMap[$key] !== null) {
+ $existingTable = $this->aliasTableMap[$key];
+ $existingColumn = $this->aliasColumnMap[$key];
+ $this->aliasTableMap[$existingTable . '.' . $key] = $existingTable;
+ $this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
+ $this->aliasTableMap[$key] = null;
+ $this->aliasColumnMap[$key] = null;
+ }
+
+ $this->aliasTableMap[$table . '.' . $key] = $table;
+ $this->aliasColumnMap[$table . '.' . $key] = $column;
+ } else {
+ $this->aliasTableMap[$key] = $table;
+ $this->aliasColumnMap[$key] = $column;
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 = new RepositoryQuery($this);
+ $query->from($this->getBaseTable(), $columns);
+ return $query;
+ }
+
+ /**
+ * Return whether this repository is capable of converting values for the given table
+ *
+ * @param string $table
+ *
+ * @return bool
+ */
+ public function providesValueConversion($table)
+ {
+ $conversionRules = $this->getConversionRules();
+ return !empty($conversionRules) && isset($conversionRules[$table]);
+ }
+
+ /**
+ * Convert a value supposed to be transmitted to the data source
+ *
+ * @param string $table The table where to persist the value
+ * @param string $name The alias or column name
+ * @param mixed $value The value to convert
+ *
+ * @return mixed If conversion was possible, the converted value, otherwise the unchanged value
+ */
+ public function persistColumn($table, $name, $value)
+ {
+ $converter = $this->getConverter($table, $name, 'persist');
+ if ($converter !== null) {
+ $value = $this->$converter($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert a value which was fetched from the data source
+ *
+ * @param string $table The table the value has been fetched from
+ * @param string $name The alias or column name
+ * @param mixed $value The value to convert
+ *
+ * @return mixed If conversion was possible, the converted value, otherwise the unchanged value
+ */
+ public function retrieveColumn($table, $name, $value)
+ {
+ $converter = $this->getConverter($table, $name, 'retrieve');
+ if ($converter !== null) {
+ $value = $this->$converter($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Return the name of the conversion method for the given alias or column name and context
+ *
+ * @param string $table The datasource's table
+ * @param string $name The alias or column name for which to return a conversion method
+ * @param string $context The context of the conversion: persist or retrieve
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case a conversion rule is found but not any conversion method
+ */
+ protected function getConverter($table, $name, $context)
+ {
+ $conversionRules = $this->getConversionRules();
+ if (! isset($conversionRules[$table])) {
+ return;
+ }
+
+ $tableRules = $conversionRules[$table];
+
+ // Check for a conversion method for the alias/column first
+ if (array_key_exists($name, $tableRules) || in_array($name, $tableRules)) {
+ $methodName = $context . join('', array_map('ucfirst', explode('_', $name)));
+ if (method_exists($this, $methodName)) {
+ return $methodName;
+ }
+ }
+
+ // The conversion method for the type is just a fallback, but it is required to exist if defined
+ if (isset($tableRules[$name])) {
+ $identifier = join('', array_map('ucfirst', explode('_', $tableRules[$name])));
+ if (! method_exists($this, $context . $identifier)) {
+ // Do not throw an error in case at least one conversion method exists
+ if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) {
+ throw new ProgrammingError(
+ 'Cannot find any conversion method for type "%s"'
+ . '. Add a proper conversion method or remove the type definition',
+ $tableRules[$name]
+ );
+ }
+
+ Logger::debug(
+ 'Conversion method "%s" for type definition "%s" does not exist in repository "%s".',
+ $context . $identifier,
+ $tableRules[$name],
+ $this->getName()
+ );
+ } else {
+ return $context . $identifier;
+ }
+ }
+ }
+
+ /**
+ * Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT
+ *
+ * @param mixed $value
+ *
+ * @return string
+ */
+ protected function persistDateTime($value)
+ {
+ if (is_numeric($value)) {
+ $value = date(static::DATETIME_FORMAT, $value);
+ } elseif ($value instanceof DateTime) {
+ $value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone
+ } elseif ($value !== null) {
+ throw new ProgrammingError(
+ 'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object',
+ $value
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp
+ *
+ * @param string $value
+ *
+ * @return int
+ */
+ protected function retrieveDateTime($value)
+ {
+ if (is_numeric($value)) {
+ $value = (int) $value;
+ } elseif (is_string($value)) {
+ $dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value);
+ if ($dateTime === false) {
+ Logger::debug(
+ 'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"',
+ $value,
+ static::DATETIME_FORMAT,
+ $this->getName()
+ );
+ $value = null;
+ } else {
+ $value = $dateTime->getTimestamp();
+ }
+ } elseif ($value !== null) {
+ throw new ProgrammingError(
+ 'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string',
+ $value
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the given array to an comma separated string
+ *
+ * @param array|string $value
+ *
+ * @return string
+ */
+ protected function persistCommaSeparatedString($value)
+ {
+ if (is_array($value)) {
+ $value = join(',', array_map('trim', $value));
+ } elseif ($value !== null && !is_string($value)) {
+ throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Convert the given comma separated string to an array
+ *
+ * @param string $value
+ *
+ * @return array
+ */
+ protected function retrieveCommaSeparatedString($value)
+ {
+ if ($value && is_string($value)) {
+ $value = String::trimSplit($value);
+ } elseif ($value !== null) {
+ throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Validate that the requested table exists
+ *
+ * @param string $table The table to validate
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (unused by the base implementation)
+ *
+ * @return string The table's name, may differ from the given one
+ *
+ * @throws ProgrammingError In case the given table does not exist
+ */
+ public function requireTable($table, RepositoryQuery $query = null)
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (! isset($queryColumns[$table])) {
+ throw new ProgrammingError('Table "%s" not found', $table);
+ }
+
+ return $table;
+ }
+
+ /**
+ * Recurse the given filter, require each column for the given table and convert all values
+ *
+ * @param string $table The table being filtered
+ * @param Filter $filter The filter to recurse
+ * @param RepositoryQuery $query An optional query to pass as context
+ * (Directly passed through to $this->requireFilterColumn)
+ * @param bool $clone Whether to clone $filter first
+ *
+ * @return Filter The udpated filter
+ */
+ public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
+ {
+ if ($clone) {
+ $filter = clone $filter;
+ }
+
+ if ($filter->isExpression()) {
+ $column = $filter->getColumn();
+ $filter->setColumn($this->requireFilterColumn($table, $column, $query));
+ $filter->setExpression($this->persistColumn($table, $column, $filter->getExpression()));
+ } elseif ($filter->isChain()) {
+ foreach ($filter->filters() as $chainOrExpression) {
+ $this->requireFilter($table, $chainOrExpression, $query, false);
+ }
+ }
+
+ return $filter;
+ }
+
+ /**
+ * Return this repository's query columns of the given table mapped to their respective aliases
+ *
+ * @param string $table
+ *
+ * @return array
+ *
+ * @throws ProgrammingError In case $table does not exist
+ */
+ public function requireAllQueryColumns($table)
+ {
+ $queryColumns = $this->getQueryColumns();
+ if (! array_key_exists($table, $queryColumns)) {
+ throw new ProgrammingError('Table name "%s" not found', $table);
+ }
+
+ $filterColumns = $this->getFilterColumns();
+ $columns = array();
+ foreach ($queryColumns[$table] as $alias => $column) {
+ if (! in_array(is_string($alias) ? $alias : $column, $filterColumns)) {
+ $columns[$alias] = $column;
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Return the query column name for the given alias or null in case the alias does not exist
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return string|null
+ */
+ public function resolveQueryColumnAlias($table, $alias)
+ {
+ $aliasColumnMap = $this->getAliasColumnMap();
+ if (isset($aliasColumnMap[$alias])) {
+ return $aliasColumnMap[$alias];
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ if (isset($aliasColumnMap[$prefixedAlias])) {
+ return $aliasColumnMap[$prefixedAlias];
+ }
+ }
+
+ /**
+ * Return whether the given alias or query column name is available in the given table
+ *
+ * @param string $table
+ * @param string $alias
+ *
+ * @return bool
+ */
+ public function validateQueryColumnAssociation($table, $alias)
+ {
+ $aliasTableMap = $this->getAliasTableMap();
+ if (isset($aliasTableMap[$alias])) {
+ return $aliasTableMap[$alias] === $table;
+ }
+
+ $prefixedAlias = $table . '.' . $alias;
+ return isset($aliasTableMap[$prefixedAlias]);
+ }
+
+ /**
+ * Return whether the given column name or alias is a valid query column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasQueryColumn($table, $name)
+ {
+ if (in_array($name, $this->getFilterColumns())) {
+ return false;
+ }
+
+ return $this->resolveQueryColumnAlias($table, $name) !== null
+ && $this->validateQueryColumnAssociation($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid query target and return it or the actual name if it's an alias
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid query column
+ */
+ public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if (in_array($name, $this->getFilterColumns())) {
+ throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
+ }
+
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
+ throw new QueryException(t('Query column "%s" not found'), $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $name)) {
+ throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return whether the given column name or alias is a valid filter column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasFilterColumn($table, $name)
+ {
+ return $this->resolveQueryColumnAlias($table, $name) !== null
+ && $this->validateQueryColumnAssociation($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid filter target and return it or the actual name if it's an alias
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The name or alias of the column to validate
+ * @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
+ *
+ * @return string The given column's name
+ *
+ * @throws QueryException In case the given column is not a valid filter column
+ */
+ public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
+ {
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
+ throw new QueryException(t('Filter column "%s" not found'), $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $name)) {
+ throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Return whether the given column name or alias of the given table is a valid statement column
+ *
+ * @param string $table The table where to look for the column or alias
+ * @param string $name The column name or alias to check
+ *
+ * @return bool
+ */
+ public function hasStatementColumn($table, $name)
+ {
+ return $this->hasQueryColumn($table, $name);
+ }
+
+ /**
+ * Validate that the given column is a valid statement column and return it or the actual name if it's an alias
+ *
+ * @param string $table The table for which to require the column
+ * @param string $name The name or alias of the column to validate
+ *
+ * @return string The given column's name
+ *
+ * @throws StatementException In case the given column is not a statement column
+ */
+ public function requireStatementColumn($table, $name)
+ {
+ if (in_array($name, $this->filterColumns)) {
+ throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
+ }
+
+ if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
+ throw new StatementException('Statement column "%s" not found', $name);
+ }
+
+ if (! $this->validateQueryColumnAssociation($table, $name)) {
+ throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
+ }
+
+ return $column;
+ }
+
+ /**
+ * Resolve the given aliases or column names of the given table supposed to be persisted and convert their values
+ *
+ * @param string $table
+ * @param array $data
+ *
+ * @return array
+ */
+ public function requireStatementColumns($table, array $data)
+ {
+ $resolved = array();
+ foreach ($data as $alias => $value) {
+ $resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value);
+ }
+
+ return $resolved;
+ }
+}
diff --git a/library/Icinga/Repository/RepositoryQuery.php b/library/Icinga/Repository/RepositoryQuery.php
new file mode 100644
index 000000000..c5e9ac9ed
--- /dev/null
+++ b/library/Icinga/Repository/RepositoryQuery.php
@@ -0,0 +1,589 @@
+repository = $repository;
+ }
+
+ /**
+ * Return the real query being used
+ *
+ * @return QueryInterface
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Set where to fetch which columns
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target from which to fetch the columns
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function from($target, array $columns = null)
+ {
+ $target = $this->repository->requireTable($target, $this);
+ $this->query = $this->repository->getDataSource()->select()->from($target);
+ $this->query->columns($this->prepareQueryColumns($target, $columns));
+ $this->target = $target;
+ return $this;
+ }
+
+ /**
+ * Return the columns to fetch
+ *
+ * @return array
+ */
+ public function getColumns()
+ {
+ return $this->query->getColumns();
+ }
+
+ /**
+ * Set which columns to fetch
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param array $columns If null or an empty array, all columns will be fetched
+ *
+ * @return $this
+ */
+ public function columns(array $columns)
+ {
+ $this->query->columns($this->prepareQueryColumns($this->target, $columns));
+ return $this;
+ }
+
+ /**
+ * Resolve the given columns supposed to be fetched
+ *
+ * This notifies the repository about each desired query column.
+ *
+ * @param mixed $target The target where to look for each column
+ * @param array $desiredColumns Pass null or an empty array to require all query columns
+ *
+ * @return array The desired columns indexed by their respective alias
+ */
+ protected function prepareQueryColumns($target, array $desiredColumns = null)
+ {
+ if (empty($desiredColumns)) {
+ $columns = $this->repository->requireAllQueryColumns($target);
+ } else {
+ $columns = array();
+ foreach ($desiredColumns as $customAlias => $columnAlias) {
+ $resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
+ if ($resolvedColumn !== $columnAlias) {
+ $columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn;
+ } elseif (is_string($customAlias)) {
+ $columns[$customAlias] = $columnAlias;
+ } else {
+ $columns[] = $columnAlias;
+ }
+ }
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Filter this query using the given column and value
+ *
+ * This notifies the repository about the required filter column.
+ *
+ * @param string $column
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function where($column, $value = null)
+ {
+ $this->query->where(
+ $this->repository->requireFilterColumn($this->target, $column, $this),
+ $this->repository->persistColumn($this->target, $column, $value)
+ );
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function applyFilter(Filter $filter)
+ {
+ return $this->addFilter($filter);
+ }
+
+ /**
+ * Set a filter for this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function setFilter(Filter $filter)
+ {
+ $this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Add an additional filter expression to this query
+ *
+ * This notifies the repository about each required filter column.
+ *
+ * @param Filter $filter
+ *
+ * @return $this
+ */
+ public function addFilter(Filter $filter)
+ {
+ $this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this));
+ return $this;
+ }
+
+ /**
+ * Return the current filter
+ *
+ * @return Filter
+ */
+ public function getFilter()
+ {
+ return $this->query->getFilter();
+ }
+
+ /**
+ * Add a sort rule for this query
+ *
+ * If called without a specific column, the repository's defaul sort rules will be applied.
+ * This notifies the repository about each column being required as filter column.
+ *
+ * @param string $field The name of the column by which to sort the query's result
+ * @param string $direction The direction to use when sorting (asc or desc, default is asc)
+ *
+ * @return $this
+ */
+ public function order($field = null, $direction = null)
+ {
+ $sortRules = $this->repository->getSortRules();
+ if ($field === null) {
+ // Use first available sort rule as default
+ if (empty($sortRules)) {
+ // Return early in case of no sort defaults and no given $field
+ return $this;
+ }
+
+ $sortColumns = reset($sortRules);
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array(key($sortRules));
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } elseif (array_key_exists($field, $sortRules)) {
+ $sortColumns = $sortRules[$field];
+ if (! array_key_exists('columns', $sortColumns)) {
+ $sortColumns['columns'] = array($field);
+ }
+ if ($direction !== null || !array_key_exists('order', $sortColumns)) {
+ $sortColumns['order'] = $direction ?: static::SORT_ASC;
+ }
+ } else {
+ $sortColumns = array(
+ 'columns' => array($field),
+ 'order' => $direction
+ );
+ };
+
+ $baseDirection = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC;
+
+ foreach ($sortColumns['columns'] as $column) {
+ list($column, $specificDirection) = $this->splitOrder($column);
+
+ try {
+ $this->query->order(
+ $this->repository->requireFilterColumn($this->target, $column, $this),
+ $specificDirection ?: $baseDirection
+ // I would have liked the following solution, but hey, a coder should be allowed to produce crap...
+ // $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection
+ );
+ } catch (QueryException $_) {
+ Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName());
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Extract and return the name and direction of the given sort column definition
+ *
+ * @param string $field
+ *
+ * @return array An array of two items: $columnName, $direction
+ */
+ protected function splitOrder($field)
+ {
+ $columnAndDirection = explode(' ', $field, 2);
+ if (count($columnAndDirection) === 1) {
+ $column = $field;
+ $direction = null;
+ } else {
+ $column = $columnAndDirection[0];
+ $direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC
+ ? static::SORT_DESC
+ : static::SORT_ASC;
+ }
+
+ return array($column, $direction);
+ }
+
+ /**
+ * Return whether any sort rules were applied to this query
+ *
+ * @return bool
+ */
+ public function hasOrder()
+ {
+ return $this->query->hasOrder();
+ }
+
+ /**
+ * Return the sort rules applied to this query
+ *
+ * @return array
+ */
+ public function getOrder()
+ {
+ return $this->query->getOrder();
+ }
+
+ /**
+ * Limit this query's results
+ *
+ * @param int $count When to stop returning results
+ * @param int $offset When to start returning results
+ *
+ * @return $this
+ */
+ public function limit($count = null, $offset = null)
+ {
+ $this->query->limit($count, $offset);
+ return $this;
+ }
+
+ /**
+ * Return whether this query does not return all available entries from its result
+ *
+ * @return bool
+ */
+ public function hasLimit()
+ {
+ return $this->query->hasLimit();
+ }
+
+ /**
+ * Return the limit when to stop returning results
+ *
+ * @return int
+ */
+ public function getLimit()
+ {
+ return $this->query->getLimit();
+ }
+
+ /**
+ * Return whether this query does not start returning results at the very first entry
+ *
+ * @return bool
+ */
+ public function hasOffset()
+ {
+ return $this->query->hasOffset();
+ }
+
+ /**
+ * Return the offset when to start returning results
+ *
+ * @return int
+ */
+ public function getOffset()
+ {
+ return $this->query->getOffset();
+ }
+
+ /**
+ * Fetch and return the first column of this query's first row
+ *
+ * @return mixed|false False in case of no result
+ */
+ public function fetchOne()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchOne();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $column = isset($columns[0]) ? $columns[0] : key($columns);
+ return $this->repository->retrieveColumn($this->target, $column, $result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first row of this query's result
+ *
+ * @return object|false False in case of no result
+ */
+ public function fetchRow()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $result = $this->query->fetchRow();
+ if ($result !== false && $this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $result->$alias = $this->repository->retrieveColumn($this->target, $alias, $result->$alias);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Fetch and return the first column of all rows of the result set as an array
+ *
+ * @return array
+ */
+ public function fetchColumn()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchColumn();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $column = is_int($aliases[0]) ? $columns[0] : $aliases[0];
+ foreach ($results as & $value) {
+ $value = $this->repository->retrieveColumn($this->target, $column, $value);
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all rows of this query's result set as an array of key-value pairs
+ *
+ * The first column is the key, the second column is the value.
+ *
+ * @return array
+ */
+ public function fetchPairs()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchPairs();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ $aliases = array_keys($columns);
+ $newResults = array();
+ foreach ($results as $colOneValue => $colTwoValue) {
+ $colOne = $aliases[0] !== 0 ? $aliases[0] : $columns[0];
+ $colTwo = count($aliases) < 2 ? $colOne : ($aliases[1] !== 1 ? $aliases[1] : $columns[1]);
+ $colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue);
+ $newResults[$colOneValue] = $this->repository->retrieveColumn($this->target, $colTwo, $colTwoValue);
+ }
+
+ $results = $newResults;
+ }
+
+ return $results;
+ }
+
+ /**
+ * Fetch and return all results of this query
+ *
+ * @return array
+ */
+ public function fetchAll()
+ {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $results = $this->query->fetchAll();
+ if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
+ $columns = $this->getColumns();
+ foreach ($results as $row) {
+ foreach ($columns as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias);
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Count all results of this query
+ *
+ * @return int
+ */
+ public function count()
+ {
+ return $this->query->count();
+ }
+
+ /**
+ * Start or rewind the iteration
+ */
+ public function rewind()
+ {
+ if ($this->iterator === null) {
+ if (! $this->hasOrder()) {
+ $this->order();
+ }
+
+ $iterator = $this->repository->getDataSource()->query($this->query);
+ if ($iterator instanceof IteratorAggregate) {
+ $this->iterator = $iterator->getIterator();
+ } else {
+ $this->iterator = $iterator;
+ }
+ }
+
+ $this->iterator->rewind();
+ Benchmark::measure('Query result iteration started');
+ }
+
+ /**
+ * Fetch and return the current row of this query's result
+ *
+ * @return object
+ */
+ public function current()
+ {
+ $row = $this->iterator->current();
+ if ($this->repository->providesValueConversion($this->target)) {
+ foreach ($this->getColumns() as $alias => $column) {
+ if (! is_string($alias)) {
+ $alias = $column;
+ }
+
+ $row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias);
+ }
+ }
+
+ return $row;
+ }
+
+ /**
+ * Return whether the current row of this query's result is valid
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ if (! $this->iterator->valid()) {
+ Benchmark::measure('Query result iteration finished');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the key for the current row of this query's result
+ *
+ * @return mixed
+ */
+ public function key()
+ {
+ return $this->iterator->key();
+ }
+
+ /**
+ * Advance to the next row of this query's result
+ */
+ public function next()
+ {
+ $this->iterator->next();
+ }
+}
diff --git a/library/Icinga/User.php b/library/Icinga/User.php
index f9df0d664..c322a04d0 100644
--- a/library/Icinga/User.php
+++ b/library/Icinga/User.php
@@ -426,7 +426,7 @@ class User
// matches
$any = strpos($requiredPermission, '*');
foreach ($this->permissions as $grantedPermission) {
- if ($any !== false && strpos($grantedPermission, '*') === false) {
+ if ($any !== false) {
$wildcard = $any;
} else {
// If the permit contains a wildcard, grant the permission if it's related to the permit
diff --git a/library/Icinga/Web/Controller/AuthBackendController.php b/library/Icinga/Web/Controller/AuthBackendController.php
new file mode 100644
index 000000000..43ed78fd7
--- /dev/null
+++ b/library/Icinga/Web/Controller/AuthBackendController.php
@@ -0,0 +1,198 @@
+hasPermission('config/authentication/users/show')) {
+ $this->redirectNow('user/list');
+ } elseif ($this->hasPermission('config/authentication/groups/show')) {
+ $this->redirectNow('group/list');
+ } elseif ($this->hasPermission('config/authentication/roles/show')) {
+ $this->redirectNow('role/list');
+ } else {
+ throw new SecurityException($this->translate('No permission for authentication configuration'));
+ }
+ }
+
+ /**
+ * Return all user backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('authentication') as $backendName => $backendConfig) {
+ $candidate = UserBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ if ($name !== null) {
+ $config = Config::app('authentication');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name));
+ } else {
+ $backend = UserBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('Authentication backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+
+ /**
+ * Return all user group backends implementing the given interface
+ *
+ * @param string $interface The class path of the interface, or null if no interface check should be made
+ *
+ * @return array
+ */
+ protected function loadUserGroupBackends($interface = null)
+ {
+ $backends = array();
+ foreach (Config::app('groups') as $backendName => $backendConfig) {
+ $candidate = UserGroupBackend::create($backendName, $backendConfig);
+ if (! $interface || $candidate instanceof $interface) {
+ $backends[] = $candidate;
+ }
+ }
+
+ return $backends;
+ }
+
+ /**
+ * Return the given user group backend or the first match in order
+ *
+ * @param string $name The name of the backend, or null in case the first match should be returned
+ * @param string $interface The interface the backend should implement, no interface check if null
+ *
+ * @return UserGroupBackendInterface
+ *
+ * @throws Zend_Controller_Action_Exception In case the given backend name is invalid
+ */
+ protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable')
+ {
+ if ($name !== null) {
+ $config = Config::app('groups');
+ if (! $config->hasSection($name)) {
+ $this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name));
+ } else {
+ $backend = UserGroupBackend::create($name, $config->getSection($name));
+ if ($interface && !$backend instanceof $interface) {
+ $interfaceParts = explode('\\', strtolower($interface));
+ throw new Zend_Controller_Action_Exception(
+ sprintf(
+ $this->translate('User group backend "%s" is not %s'),
+ $name,
+ array_pop($interfaceParts)
+ ),
+ 400
+ );
+ }
+ }
+ } else {
+ $backends = $this->loadUserGroupBackends($interface);
+ $backend = array_shift($backends);
+ }
+
+ return $backend;
+ }
+
+ /**
+ * Create the tabs to list users and groups
+ */
+ protected function createListTabs()
+ {
+ $tabs = $this->getTabs();
+
+ if ($this->hasPermission('config/authentication/users/show')) {
+ $tabs->add(
+ 'user/list',
+ array(
+ 'title' => $this->translate('List users of authentication backends'),
+ 'label' => $this->translate('Users'),
+ 'icon' => 'user',
+ 'url' => 'user/list'
+ )
+ );
+ }
+
+ if ($this->hasPermission('config/authentication/groups/show')) {
+ $tabs->add(
+ 'group/list',
+ array(
+ 'title' => $this->translate('List groups of user group backends'),
+ 'label' => $this->translate('Groups'),
+ 'icon' => 'users',
+ 'url' => 'group/list'
+ )
+ );
+ }
+
+ if ($this->hasPermission('config/authentication/roles/show')) {
+ $tabs->add(
+ 'role/list',
+ array(
+ 'title' => $this->translate(
+ 'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
+ ),
+ 'label' => $this->translate('Roles'),
+ 'url' => 'role/list'
+ )
+ );
+ }
+
+ return $tabs;
+ }
+}
diff --git a/library/Icinga/Web/Form/ErrorLabeller.php b/library/Icinga/Web/Form/ErrorLabeller.php
index 455f5f81e..f66260149 100644
--- a/library/Icinga/Web/Form/ErrorLabeller.php
+++ b/library/Icinga/Web/Form/ErrorLabeller.php
@@ -39,7 +39,7 @@ class ErrorLabeller extends Zend_Translate_Adapter
protected function createMessages($element)
{
- $label = $element->getLabel();
+ $label = $element->getLabel() ?: $element->getName();
return array(
Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
diff --git a/library/Icinga/Web/Menu.php b/library/Icinga/Web/Menu.php
index 476d73879..63d92fc9f 100644
--- a/library/Icinga/Web/Menu.php
+++ b/library/Icinga/Web/Menu.php
@@ -233,40 +233,50 @@ class Menu implements RecursiveIterator
));
$section = $this->add(t('System'), array(
- 'icon' => 'wrench',
- 'priority' => 200,
+ 'icon' => 'services',
+ 'priority' => 700,
'renderer' => 'ProblemMenuItemRenderer'
));
- $section->add(t('Configuration'), array(
+ if (Logger::writesToFile()) {
+ $section->add(t('Application Log'), array(
+ 'url' => 'list/applicationlog',
+ 'priority' => 710
+ ));
+ }
+
+ $section = $this->add(t('Configuration'), array(
+ 'icon' => 'wrench',
+ 'permission' => 'config/*',
+ 'priority' => 800
+ ));
+ $section->add(t('Application'), array(
'url' => 'config',
'permission' => 'config/application/*',
- 'priority' => 300
+ 'priority' => 810
+ ));
+ $section->add(t('Authentication'), array(
+ 'url' => 'user',
+ 'permission' => 'config/authentication/*',
+ 'priority' => 820
));
$section->add(t('Modules'), array(
'url' => 'config/modules',
'permission' => 'config/modules',
- 'priority' => 400
+ 'priority' => 890
));
- if (Logger::writesToFile()) {
- $section->add(t('Application Log'), array(
- 'url' => 'list/applicationlog',
- 'priority' => 500
- ));
- }
-
$section = $this->add($auth->getUser()->getUsername(), array(
'icon' => 'user',
- 'priority' => 600
+ 'priority' => 900
));
$section->add(t('Preferences'), array(
'url' => 'preference',
- 'priority' => 601
+ 'priority' => 910
));
$section->add(t('Logout'), array(
'url' => 'authentication/logout',
- 'priority' => 700,
+ 'priority' => 990,
'renderer' => 'ForeignMenuItemRenderer'
));
}
diff --git a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
index 5d85a4428..3f2b71cc3 100644
--- a/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
+++ b/library/Icinga/Web/Paginator/Adapter/QueryAdapter.php
@@ -4,44 +4,64 @@
namespace Icinga\Web\Paginator\Adapter;
use Zend_Paginator_Adapter_Interface;
-
-/**
- * @see Zend_Paginator_Adapter_Interface
- */
+use Icinga\Data\QueryInterface;
class QueryAdapter implements Zend_Paginator_Adapter_Interface
{
/**
- * Array
+ * The query being paginated
*
- * @var array
+ * @var QueryInterface
*/
- protected $query = null;
+ protected $query;
/**
* Item count
*
- * @var integer
+ * @var int
*/
- protected $count = null;
+ protected $count;
/**
- * Constructor.
+ * Create a new QueryAdapter
*
- * @param array $query Query to paginate
+ * @param QueryInterface $query The query to paginate
*/
- // TODO: This might be ready for (QueryInterface $query)
- public function __construct($query)
+ public function __construct(QueryInterface $query)
{
- $this->query = $query;
+ $this->setQuery($query);
}
/**
- * Returns an array of items for a page.
+ * Set the query to paginate
*
- * @param integer $offset Page offset
- * @param integer $itemCountPerPage Number of items per page
- * @return array
+ * @param QueryInterface $query
+ *
+ * @return $this
+ */
+ public function setQuery(QueryInterface $query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Return the query being paginated
+ *
+ * @return QueryInterface
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Fetch and return the rows in the given range of the query result
+ *
+ * @param int $offset Page offset
+ * @param int $itemCountPerPage Number of items per page
+ *
+ * @return array
*/
public function getItems($offset, $itemCountPerPage)
{
@@ -49,15 +69,16 @@ class QueryAdapter implements Zend_Paginator_Adapter_Interface
}
/**
- * Returns the total number of items in the query result.
+ * Return the total number of items in the query result
*
- * @return integer
+ * @return int
*/
public function count()
{
if ($this->count === null) {
$this->count = $this->query->count();
}
+
return $this->count;
}
}
diff --git a/modules/doc/configuration.php b/modules/doc/configuration.php
index 031a62e64..392009798 100644
--- a/modules/doc/configuration.php
+++ b/modules/doc/configuration.php
@@ -7,7 +7,7 @@ $section = $this->menuSection($this->translate('Documentation'), array(
'title' => 'Documentation',
'icon' => 'book',
'url' => 'doc',
- 'priority' => 190
+ 'priority' => 700
));
$section->add('Icinga Web 2', array(
@@ -18,7 +18,7 @@ $section->add('Module documentations', array(
));
$section->add($this->translate('Developer - Style'), array(
'url' => 'doc/style/guide',
- 'priority' => 200,
+ 'priority' => 790
));
$this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10);
diff --git a/modules/monitoring/configuration.php b/modules/monitoring/configuration.php
index fd3ee334d..66b42aa20 100644
--- a/modules/monitoring/configuration.php
+++ b/modules/monitoring/configuration.php
@@ -208,7 +208,7 @@ $section->add($this->translate('Alert Summary'), array(
$section = $this->menuSection($this->translate('System'));
$section->add($this->translate('Monitoring Health'), array(
'url' => 'monitoring/process/info',
- 'priority' => 120,
+ 'priority' => 720,
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer'
));
diff --git a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
index 9371399bc..0b436d7bf 100644
--- a/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
+++ b/modules/monitoring/library/Monitoring/Backend/Ido/Query/IdoQuery.php
@@ -415,21 +415,11 @@ abstract class IdoQuery extends DbQuery
} elseif ($dbType === 'pgsql') {
$this->initializeForPostgres();
}
- $this->dbSelect();
+ $this->joinBaseTables();
$this->select->columns($this->columns);
- //$this->joinBaseTables();
$this->prepareAliasIndexes();
}
- protected function dbSelect()
- {
- if ($this->select === null) {
- $this->select = $this->db->select();
- $this->joinBaseTables();
- }
- return clone $this->select;
- }
-
/**
* Join the base tables for this query
*/
diff --git a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
index a9839565b..f39b0b4c3 100644
--- a/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
+++ b/modules/monitoring/library/Monitoring/Command/Transport/CommandTransport.php
@@ -108,7 +108,8 @@ abstract class CommandTransport
*/
public static function first()
{
- $config = self::getConfig()->current();
- return self::fromConfig($config);
+ $config = self::getConfig();
+ $config->rewind();
+ return self::fromConfig($config->current());
}
}
diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php
index 69d8631c8..ba628f31c 100644
--- a/modules/monitoring/library/Monitoring/DataView/DataView.php
+++ b/modules/monitoring/library/Monitoring/DataView/DataView.php
@@ -3,7 +3,6 @@
namespace Icinga\Module\Monitoring\DataView;
-use ArrayIterator;
use IteratorAggregate;
use Icinga\Data\QueryInterface;
use Icinga\Data\Filter\Filter;
@@ -13,6 +12,7 @@ use Icinga\Data\ConnectionInterface;
use Icinga\Exception\QueryException;
use Icinga\Web\Request;
use Icinga\Web\Url;
+use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
/**
@@ -23,7 +23,7 @@ abstract class DataView implements QueryInterface, IteratorAggregate
/**
* The query used to populate the view
*
- * @var QueryInterface
+ * @var IdoQuery
*/
protected $query;
@@ -61,11 +61,11 @@ abstract class DataView implements QueryInterface, IteratorAggregate
/**
* Return a iterator for all rows of the result set
*
- * @return ArrayIterator
+ * @return IdoQuery
*/
public function getIterator()
{
- return new ArrayIterator($this->fetchAll());
+ return $this->getQuery();
}
/**
@@ -481,15 +481,13 @@ abstract class DataView implements QueryInterface, IteratorAggregate
}
/**
- * Fetch a column of all rows of the result set as an array
- *
- * @param int $columnIndex Index of the column to fetch
+ * Fetch the first column of all rows of the result set as an array
*
* @return array
*/
- public function fetchColumn($columnIndex = 0)
+ public function fetchColumn()
{
- return $this->getQuery()->fetchColumn($columnIndex);
+ return $this->getQuery()->fetchColumn();
}
/**
diff --git a/modules/setup/application/forms/AdminAccountPage.php b/modules/setup/application/forms/AdminAccountPage.php
index b123b142d..a7bd8fc6c 100644
--- a/modules/setup/application/forms/AdminAccountPage.php
+++ b/modules/setup/application/forms/AdminAccountPage.php
@@ -8,8 +8,8 @@ use LogicException;
use Icinga\Web\Form;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
-use Icinga\Authentication\Backend\DbUserBackend;
-use Icinga\Authentication\Backend\LdapUserBackend;
+use Icinga\Authentication\User\DbUserBackend;
+use Icinga\Authentication\User\LdapUserBackend;
/**
* Wizard page to define the initial administrative account
@@ -268,13 +268,8 @@ class AdminAccountPage extends Form
if ($this->backendConfig['backend'] === 'db') {
$backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
} elseif ($this->backendConfig['backend'] === 'ldap') {
- $backend = new LdapUserBackend(
- ResourceFactory::createResource(new ConfigObject($this->resourceConfig)),
- $this->backendConfig['user_class'],
- $this->backendConfig['user_name_attribute'],
- $this->backendConfig['base_dn'],
- $this->backendConfig['filter']
- );
+ $backend = new LdapUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
+ $backend->setConfig($this->backendConfig);
} else {
throw new LogicException(
sprintf(
@@ -285,10 +280,8 @@ class AdminAccountPage extends Form
}
try {
- $users = $backend->listUsers();
- natsort ($users);
- return $users;
- } catch (Exception $e) {
+ return $backend->select(array('user_name'))->fetchColumn();
+ } catch (Exception $_) {
// No need to handle anything special here. Error means no users found.
return array();
}
diff --git a/modules/setup/application/forms/AuthBackendPage.php b/modules/setup/application/forms/AuthBackendPage.php
index f3bce41fa..bb68792a6 100644
--- a/modules/setup/application/forms/AuthBackendPage.php
+++ b/modules/setup/application/forms/AuthBackendPage.php
@@ -4,9 +4,9 @@
namespace Icinga\Module\Setup\Forms;
use Icinga\Web\Form;
-use Icinga\Forms\Config\Authentication\DbBackendForm;
-use Icinga\Forms\Config\Authentication\LdapBackendForm;
-use Icinga\Forms\Config\Authentication\ExternalBackendForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
use Icinga\Data\ConfigObject;
/**
@@ -105,7 +105,7 @@ class AuthBackendPage extends Form
}
if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) {
- if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidAuthenticationBackend($this)) {
+ if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidUserBackend($this)) {
$this->addSkipValidationCheckbox();
return false;
}
diff --git a/modules/setup/library/Setup/Steps/AuthenticationStep.php b/modules/setup/library/Setup/Steps/AuthenticationStep.php
index 9de0c9867..58d9d68d7 100644
--- a/modules/setup/library/Setup/Steps/AuthenticationStep.php
+++ b/modules/setup/library/Setup/Steps/AuthenticationStep.php
@@ -7,7 +7,7 @@ use Exception;
use Icinga\Application\Config;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
-use Icinga\Authentication\Backend\DbUserBackend;
+use Icinga\Authentication\User\DbUserBackend;
use Icinga\Module\Setup\Step;
class AuthenticationStep extends Step
@@ -88,11 +88,12 @@ class AuthenticationStep extends Step
ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig']))
);
- if (array_search($this->data['adminAccountData']['username'], $backend->listUsers()) === false) {
- $backend->addUser(
- $this->data['adminAccountData']['username'],
- $this->data['adminAccountData']['password']
- );
+ if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) {
+ $backend->insert('user', array(
+ 'user_name' => $this->data['adminAccountData']['username'],
+ 'password' => $this->data['adminAccountData']['password'],
+ 'is_active' => true
+ ));
}
} catch (Exception $e) {
$this->dbError = $e;
diff --git a/public/css/icinga/main-content.less b/public/css/icinga/main-content.less
index b442723cb..2b0e2e50d 100644
--- a/public/css/icinga/main-content.less
+++ b/public/css/icinga/main-content.less
@@ -41,6 +41,10 @@ img.icon {
background-position: 1em center;
}
+#notifications > li.info {
+ background-color: @colorFormNotificationInfo;
+}
+
#notifications > li.warning {
background-color: @colorWarningHandled;
}
@@ -202,3 +206,164 @@ table.benchmark {
border: 1px solid lightgrey;
background-color: #fbfcc5;
}
+
+div.content.users {
+ table.user-list {
+ th.user-remove {
+ width: 8em;
+ padding-right: 0.5em;
+ text-align: right;
+ }
+
+ td.user-remove {
+ text-align: right;
+ }
+ }
+
+ p {
+ margin-top: 0;
+ }
+
+ a.user-add {
+ display: block;
+ margin-top: 1em;
+ }
+}
+
+div.controls div.user-header {
+ border-bottom: 2px solid @colorPetrol;
+ margin-bottom: 1em;
+
+ .user-name {
+ display: inline-block;
+ margin: 0 0 0.3em;
+ font-size: 2em;
+ }
+
+ .user-state, .user-created, .user-modified {
+ margin: 0 0 0.2em;
+ font-size: 0.8em;
+ }
+}
+
+div.content.memberships {
+ table.membership-list {
+ th.membership-cancel {
+ width: 8em;
+ padding-right: 0.5em;
+ text-align: right;
+ }
+
+ td.membership-cancel {
+ text-align: right;
+
+ form button.link-like {
+ color: inherit;
+ }
+ }
+ }
+
+ p {
+ margin-top: 0;
+ }
+
+ a.membership-create {
+ display: block;
+ margin-top: 1em;
+ }
+}
+
+div.content.groups {
+ table.group-list {
+ th.group-remove {
+ width: 8em;
+ padding-right: 0.5em;
+ text-align: right;
+ }
+
+ td.group-remove {
+ text-align: right;
+ }
+ }
+
+ p {
+ margin-top: 0;
+ }
+
+ a.group-add {
+ display: block;
+ margin-top: 1em;
+ }
+}
+
+div.controls div.group-header {
+ border-bottom: 2px solid @colorPetrol;
+ margin-bottom: 1em;
+
+ .group-name {
+ display: inline-block;
+ margin: 0 0 0.3em;
+ font-size: 2em;
+ }
+
+ .group-parent, .group-created, .group-modified {
+ margin: 0 0 0.2em;
+ font-size: 0.8em;
+ }
+}
+
+div.content.members {
+ table.member-list {
+ th.member-remove {
+ width: 8em;
+ padding-right: 0.5em;
+ text-align: right;
+ }
+
+ td.member-remove {
+ text-align: right;
+
+ form button.link-like {
+ color: inherit;
+ }
+ }
+ }
+
+ p {
+ margin-top: 0;
+ }
+
+ a.member-add {
+ display: block;
+ margin-top: 1em;
+ }
+}
+
+form.backend-selection {
+ float: right;
+
+ div.element {
+ margin: 0;
+
+ label {
+ width: auto;
+ margin-right: 0.5em;
+ }
+
+ select {
+ width: 11.5em;
+ margin-left: 0;
+ }
+ }
+}
+
+table.usergroupbackend-list {
+ th.backend-remove {
+ width: 8em;
+ text-align: right;
+ }
+
+ td.backend-remove {
+ text-align: right;
+ }
+}
\ No newline at end of file
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
index 99667ac1c..082b41e87 100644
--- a/public/js/icinga/loader.js
+++ b/public/js/icinga/loader.js
@@ -310,7 +310,7 @@
} else {
if (req.$target.attr('id') === 'col2') { // TODO: multicol
- if ($('#col1').data('icingaUrl') === redirect) {
+ if ($('#col1').data('icingaUrl').split('?')[0] === redirect.split('?')[0]) {
icinga.ui.layout1col();
req.$target = $('#col1');
delete(this.requests['col2']);
diff --git a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php
similarity index 72%
rename from test/php/application/forms/Config/Authentication/DbBackendFormTest.php
rename to test/php/application/forms/Config/UserBackend/DbBackendFormTest.php
index fb93f6050..d58ff8a33 100644
--- a/test/php/application/forms/Config/Authentication/DbBackendFormTest.php
+++ b/test/php/application/forms/Config/UserBackend/DbBackendFormTest.php
@@ -1,7 +1,7 @@
setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend')
- ->shouldReceive('count')
+ Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
+ ->shouldReceive('select->where->count')
->andReturn(2);
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -41,8 +41,8 @@ class DbBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_db_backend'));
$this->assertTrue(
- DbBackendForm::isValidAuthenticationBackend($form),
- 'DbBackendForm claims that a valid authentication backend with users is not valid'
+ DbBackendForm::isValidUserBackend($form),
+ 'DbBackendForm claims that a valid user backend with users is not valid'
);
}
@@ -53,12 +53,12 @@ class DbBackendFormTest extends BaseTestCase
public function testInvalidBackendIsNotValid()
{
$this->setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend')
+ Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
->shouldReceive('count')
->andReturn(0);
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -67,8 +67,8 @@ class DbBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_db_backend'));
$this->assertFalse(
- DbBackendForm::isValidAuthenticationBackend($form),
- 'DbBackendForm claims that an invalid authentication backend without users is valid'
+ DbBackendForm::isValidUserBackend($form),
+ 'DbBackendForm claims that an invalid user backend without users is valid'
);
}
diff --git a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
similarity index 74%
rename from test/php/application/forms/Config/Authentication/LdapBackendFormTest.php
rename to test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
index 56ea08987..f7373a7ae 100644
--- a/test/php/application/forms/Config/Authentication/LdapBackendFormTest.php
+++ b/test/php/application/forms/Config/UserBackend/LdapBackendFormTest.php
@@ -1,7 +1,7 @@
setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend')
- ->shouldReceive('assertAuthenticationPossible')->andReturnNull();
+ Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
+ ->shouldReceive('assertAuthenticationPossible')->andReturnNull()
+ ->shouldReceive('setConfig')->andReturnNull();
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -41,8 +42,8 @@ class LdapBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_ldap_backend'));
$this->assertTrue(
- LdapBackendForm::isValidAuthenticationBackend($form),
- 'LdapBackendForm claims that a valid authentication backend with users is not valid'
+ LdapBackendForm::isValidUserBackend($form),
+ 'LdapBackendForm claims that a valid user backend with users is not valid'
);
}
@@ -53,11 +54,11 @@ class LdapBackendFormTest extends BaseTestCase
public function testInvalidBackendIsNotValid()
{
$this->setUpResourceFactoryMock();
- Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend')
+ Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException);
// Passing array(null) is required to make Mockery call the constructor...
- $form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));
+ $form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
$form->shouldReceive('getView->escape')
->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; });
@@ -66,8 +67,8 @@ class LdapBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_ldap_backend'));
$this->assertFalse(
- LdapBackendForm::isValidAuthenticationBackend($form),
- 'LdapBackendForm claims that an invalid authentication backend without users is valid'
+ LdapBackendForm::isValidUserBackend($form),
+ 'LdapBackendForm claims that an invalid user backend without users is valid'
);
}
diff --git a/test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php b/test/php/application/forms/Config/UserBackendReorderFormTest.php
similarity index 64%
rename from test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php
rename to test/php/application/forms/Config/UserBackendReorderFormTest.php
index 23563f31b..240d578be 100644
--- a/test/php/application/forms/Config/AuthenticationBackendReorderFormTest.php
+++ b/test/php/application/forms/Config/UserBackendReorderFormTest.php
@@ -5,10 +5,10 @@ namespace Tests\Icinga\Forms\Config;
use Icinga\Test\BaseTestCase;
use Icinga\Application\Config;
-use Icinga\Forms\Config\AuthenticationBackendConfigForm;
-use Icinga\Forms\Config\AuthenticationBackendReorderForm;
+use Icinga\Forms\Config\UserBackendConfigForm;
+use Icinga\Forms\Config\UserBackendReorderForm;
-class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendConfigForm
+class UserBackendConfigFormWithoutSave extends UserBackendConfigForm
{
public static $newConfig;
@@ -19,11 +19,11 @@ class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendCo
}
}
-class AuthenticationBackendReorderFormProvidingConfigFormWithoutSave extends AuthenticationBackendReorderForm
+class UserBackendReorderFormProvidingConfigFormWithoutSave extends UserBackendReorderForm
{
public function getConfigForm()
{
- $form = new AuthenticationBackendConfigFormWithoutSave();
+ $form = new UserBackendConfigFormWithoutSave();
$form->setIniConfig($this->config);
return $form;
}
@@ -45,7 +45,7 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
->shouldReceive('isPost')->andReturn(true)
->shouldReceive('getPost')->andReturn(array('backend_newpos' => 'test3|1'));
- $form = new AuthenticationBackendReorderFormProvidingConfigFormWithoutSave();
+ $form = new UserBackendReorderFormProvidingConfigFormWithoutSave();
$form->setIniConfig($config);
$form->setTokenDisabled();
$form->setUidDisabled();
@@ -53,8 +53,8 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
$this->assertEquals(
array('test1', 'test3', 'test2'),
- AuthenticationBackendConfigFormWithoutSave::$newConfig->keys(),
- 'Moving elements with AuthenticationBackendReorderForm does not seem to properly work'
+ UserBackendConfigFormWithoutSave::$newConfig->keys(),
+ 'Moving elements with UserBackendReorderForm does not seem to properly work'
);
}
}
diff --git a/test/php/library/Icinga/Data/ConfigObjectTest.php b/test/php/library/Icinga/Data/ConfigObjectTest.php
index eabdf80cf..820d17b83 100644
--- a/test/php/library/Icinga/Data/ConfigObjectTest.php
+++ b/test/php/library/Icinga/Data/ConfigObjectTest.php
@@ -60,14 +60,6 @@ class ConfigObjectTest extends BaseTestCase
);
}
- public function testWhetherConfigObjectsAreCountable()
- {
- $config = new ConfigObject(array('a' => 'b', 'c' => array('d' => 'e')));
-
- $this->assertInstanceOf('Countable', $config, 'ConfigObject objects do not implement interface `Countable\'');
- $this->assertEquals(2, count($config), 'ConfigObject objects do not count properties and sections correctly');
- }
-
public function testWhetherConfigObjectsAreTraversable()
{
$config = new ConfigObject(array('a' => 'b', 'c' => 'd'));
@@ -124,7 +116,7 @@ class ConfigObjectTest extends BaseTestCase
}
/**
- * @expectedException LogicException
+ * @expectedException \Icinga\Exception\ProgrammingError
*/
public function testWhetherItIsNotPossibleToAppendProperties()
{
@@ -142,9 +134,6 @@ class ConfigObjectTest extends BaseTestCase
$this->assertFalse(isset($config->c), 'ConfigObjects do not allow to unset sections');
}
- /**
- * @depends testWhetherConfigObjectsAreCountable
- */
public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections()
{
$config = new ConfigObject();
diff --git a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php b/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php
deleted file mode 100644
index 182004703..000000000
--- a/test/php/library/Icinga/Protocol/Ldap/ConnectionTest.php
+++ /dev/null
@@ -1,258 +0,0 @@
-getAttributesMock;
- }
-
- function ldap_start_tls()
- {
- global $self;
- $self->startTlsCalled = true;
- }
-
- function ldap_set_option($ds, $option, $value)
- {
- global $self;
- $self->activatedOptions[$option] = $value;
- return true;
- }
-
- function ldap_set($ds, $option)
- {
- global $self;
- $self->activatedOptions[] = $option;
- }
-
- function ldap_control_paged_result()
- {
- global $self;
- $self->pagedResultsCalled = true;
- return true;
- }
-
- function ldap_control_paged_result_response()
- {
- return true;
- }
-
- function ldap_get_dn()
- {
- return NULL;
- }
-
- function ldap_free_result()
- {
- return NULL;
- }
- }
-
- private function node(&$element, $name)
- {
- $element['count']++;
- $element[$name] = array('count' => 0);
- $element[] = $name;
- }
-
- private function addEntry(&$element, $name, $entry)
- {
- $element[$name]['count']++;
- $element[$name][] = $entry;
- }
-
- private function mockQuery()
- {
- return Mockery::mock('overload:Icinga\Protocol\Ldap\Query')
- ->shouldReceive(array(
- 'from' => Mockery::self(),
- 'create' => array('count' => 1),
- 'listFields' => array('count' => 1),
- 'getLimit' => 1,
- 'hasOffset' => false,
- 'hasBase' => false,
- 'getSortColumns' => array(),
- 'getUsePagedResults' => true
- ));
- }
-
- private function connectionFetchAll()
- {
- $this->mockQuery();
- $this->connection->connect();
- $this->connection->fetchAll(Mockery::self());
- }
-
- public function setUp()
- {
- $this->pagedResultsCalled = false;
- $this->startTlsCalled = false;
- $this->activatedOptions = array();
-
- $this->mockLdapFunctions();
-
- $config = new ConfigObject(
- array(
- 'hostname' => 'localhost',
- 'root_dn' => 'dc=example,dc=com',
- 'bind_dn' => 'cn=user,ou=users,dc=example,dc=com',
- 'bind_pw' => '***'
- )
- );
- $this->connection = new Connection($config);
-
- $caps = array('count' => 0);
- $this->node($caps, 'defaultNamingContext');
- $this->node($caps, 'namingContexts');
- $this->node($caps, 'supportedCapabilities');
- $this->node($caps, 'supportedControl');
- $this->node($caps, 'supportedLDAPVersion');
- $this->node($caps, 'supportedExtension');
- $this->getAttributesMock = $caps;
- }
-
- public function testUsePageControlWhenAnnounced()
- {
- if (version_compare(PHP_VERSION, '5.4.0') < 0) {
- $this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0');
- }
-
- $this->addEntry($this->getAttributesMock, 'supportedControl', Capability::LDAP_PAGED_RESULT_OID_STRING);
- $this->connectionFetchAll();
-
- // see ticket #7993
- $this->assertEquals(true, $this->pagedResultsCalled, "Use paged result when capability is present.");
- }
-
- public function testDontUsePagecontrolWhenNotAnnounced()
- {
- if (version_compare(PHP_VERSION, '5.4.0') < 0) {
- $this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0');
- }
- $this->connectionFetchAll();
-
- // see ticket #8490
- $this->assertEquals(false, $this->pagedResultsCalled, "Don't use paged result when capability is not announced.");
- }
-
- public function testUseLdapV2WhenAnnounced()
- {
- // TODO: Test turned off, see other TODO in Ldap/Connection.
- $this->markTestSkipped('LdapV2 currently turned off.');
-
- $this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 2);
- $this->connectionFetchAll();
-
- $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 2);
- }
-
- public function testUseLdapV3WhenAnnounced()
- {
- $this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 3);
- $this->connectionFetchAll();
-
- $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active");
- }
-
- public function testDefaultSettings()
- {
- $this->connectionFetchAll();
-
- $this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active");
-
- $this->assertArrayHasKey(LDAP_OPT_REFERRALS, $this->activatedOptions, "Following referrals must be turned off");
- $this->assertEquals($this->activatedOptions[LDAP_OPT_REFERRALS], 0, "Following referrals must be turned off");
- }
-
-
- public function testActiveDirectoryDiscovery()
- {
- $this->addEntry($this->getAttributesMock, 'supportedCapabilities', Capability::LDAP_CAP_ACTIVE_DIRECTORY_OID);
- $this->connectionFetchAll();
-
- $this->assertEquals(true, $this->connection->getCapabilities()->hasAdOid(),
- "Server with LDAP_CAP_ACTIVE_DIRECTORY_OID must be recognized as Active Directory.");
- }
-
- public function testDefaultNamingContext()
- {
- $this->addEntry($this->getAttributesMock, 'defaultNamingContext', 'dn=default,dn=contex');
- $this->connectionFetchAll();
-
- $this->assertEquals('dn=default,dn=contex', $this->connection->getCapabilities()->getDefaultNamingContext(),
- 'Default naming context must be correctly recognized.');
- }
-
- public function testDefaultNamingContextFallback()
- {
- $this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=some,dn=other,dn=context');
- $this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=default,dn=context');
- $this->connectionFetchAll();
-
- $this->assertEquals('dn=some,dn=other,dn=context', $this->connection->getCapabilities()->getDefaultNamingContext(),
- 'If defaultNamingContext is missing, the connection must fallback to first namingContext.');
- }
-}
diff --git a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php
index 589dc8f36..44e47df78 100644
--- a/test/php/library/Icinga/Protocol/Ldap/QueryTest.php
+++ b/test/php/library/Icinga/Protocol/Ldap/QueryTest.php
@@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase
return $select;
}
- public function testLimit()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(10, $select->getLimit());
- $this->assertEquals(4, $select->getOffset());
- }
-
- public function testHasLimit()
- {
- $select = $this->emptySelect();
- $this->assertFalse($select->hasLimit());
- $select = $this->prepareSelect();
- $this->assertTrue($select->hasLimit());
- }
-
- public function testHasOffset()
- {
- $select = $this->emptySelect();
- $this->assertFalse($select->hasOffset());
- $select = $this->prepareSelect();
- $this->assertTrue($select->hasOffset());
- }
-
- public function testGetLimit()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(10, $select->getLimit());
- }
-
- public function testGetOffset()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(10, $select->getLimit());
- }
-
public function testFetchTree()
{
$this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP');
}
- public function testFrom()
- {
- return $this->testListFields();
- }
-
public function testWhere()
{
$this->markTestIncomplete('testWhere is not implemented yet');
@@ -88,30 +48,13 @@ class QueryTest extends BaseTestCase
public function testOrder()
{
- $select = $this->emptySelect()->order('bla');
- // tested by testGetSortColumns
+ $this->markTestIncomplete('testOrder is not implemented yet, order support for ldap queries is incomplete');
}
- public function testListFields()
- {
- $select = $this->prepareSelect();
- $this->assertEquals(
- array('testIntColumn', 'testStringColumn'),
- $select->listFields()
- );
- }
-
- public function testGetSortColumns()
- {
- $select = $this->prepareSelect();
- $cols = $select->getSortColumns();
- $this->assertEquals('testIntColumn', $cols[0][0]);
- }
-
- public function testCreateQuery()
+ public function testRenderFilter()
{
$select = $this->prepareSelect();
$res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))';
- $this->assertEquals($res, $select->create());
+ $this->assertEquals($res, (string) $select);
}
}
diff --git a/test/php/library/Icinga/UserTest.php b/test/php/library/Icinga/UserTest.php
index dc55dc62d..97cf412c0 100644
--- a/test/php/library/Icinga/UserTest.php
+++ b/test/php/library/Icinga/UserTest.php
@@ -67,13 +67,15 @@ class UserTest extends BaseTestCase
'test',
'test/some/specific',
'test/more/*',
- 'test/wildcard-with-wildcard/*'
+ 'test/wildcard-with-wildcard/*',
+ 'test/even-more/specific-with-wildcard/*'
));
$this->assertTrue($user->can('test'));
$this->assertTrue($user->can('test/some/specific'));
$this->assertTrue($user->can('test/more/everything'));
$this->assertTrue($user->can('test/wildcard-with-wildcard/*'));
$this->assertTrue($user->can('test/wildcard-with-wildcard/sub/sub'));
+ $this->assertTrue($user->can('test/even-more/*'));
$this->assertFalse($user->can('not/test'));
$this->assertFalse($user->can('test/some/not/so/specific'));
$this->assertFalse($user->can('test/wildcard2/*'));
|