Merge branch 'feature/user-and-group-management-8826'

resolves #8826
resolves #9122
resolves #8877
This commit is contained in:
Johannes Meyer 2015-06-02 15:38:43 +02:00
commit 5f898a3e23
97 changed files with 8343 additions and 2035 deletions

View File

@ -7,7 +7,7 @@ use Icinga\Application\Config;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Authentication\AuthChain; use Icinga\Authentication\AuthChain;
use Icinga\Authentication\Backend\ExternalBackend; use Icinga\Authentication\User\ExternalBackend;
use Icinga\Exception\AuthenticationException; use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;

View File

@ -5,14 +5,15 @@ use Icinga\Application\Config;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Application\Modules\Module; use Icinga\Application\Modules\Module;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Forms\Config\AuthenticationBackendConfigForm; use Icinga\Forms\Config\UserBackendConfigForm;
use Icinga\Forms\Config\AuthenticationBackendReorderForm; use Icinga\Forms\Config\UserBackendReorderForm;
use Icinga\Forms\Config\GeneralConfigForm; use Icinga\Forms\Config\GeneralConfigForm;
use Icinga\Forms\Config\ResourceConfigForm; use Icinga\Forms\Config\ResourceConfigForm;
use Icinga\Forms\ConfirmRemovalForm; use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Security\SecurityException; use Icinga\Security\SecurityException;
use Icinga\Web\Controller; use Icinga\Web\Controller;
use Icinga\Web\Notification; use Icinga\Web\Notification;
use Icinga\Web\Url;
use Icinga\Web\Widget; use Icinga\Web\Widget;
/** /**
@ -38,20 +39,12 @@ class ConfigController extends Controller
$auth = $this->Auth(); $auth = $this->Auth();
$allowedActions = array(); $allowedActions = array();
if ($auth->hasPermission('config/application/general')) { 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'), 'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
'label' => $this->translate('Application'), 'label' => $this->translate('General'),
'url' => 'config/application' 'url' => 'config/general'
)); ));
$allowedActions[] = 'application'; $allowedActions[] = 'general';
}
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';
} }
if ($auth->hasPermission('config/application/resources')) { if ($auth->hasPermission('config/application/resources')) {
$tabs->add('resource', array( $tabs->add('resource', array(
@ -61,15 +54,21 @@ class ConfigController extends Controller
)); ));
$allowedActions[] = 'resource'; $allowedActions[] = 'resource';
} }
if ($auth->hasPermission('config/application/roles')) { if ($auth->hasPermission('config/application/userbackend')) {
$tabs->add('roles', array( $tabs->add('userbackend', array(
'title' => $this->translate( 'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
'Configure roles to permit or restrict users and groups accessing Icinga Web 2' 'label' => $this->translate('Authentication'),
), 'url' => 'config/userbackend'
'label' => $this->translate('Roles'),
'url' => 'roles'
)); ));
$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); $this->firstAllowedAction = array_shift($allowedActions);
} }
@ -85,7 +84,7 @@ class ConfigController extends Controller
public function indexAction() public function indexAction()
{ {
if ($this->firstAllowedAction === null) { 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); $action = $this->getTabs()->get($this->firstAllowedAction);
if (substr($action->getUrl()->getPath(), 0, 7) === 'config/') { 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'); $this->assertPermission('config/application/general');
$form = new GeneralConfigForm(); $form = new GeneralConfigForm();
@ -108,7 +107,7 @@ class ConfigController extends Controller
$form->handleRequest(); $form->handleRequest();
$this->view->form = $form; $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'); $this->assertPermission('config/application/userbackend');
$form = new AuthenticationBackendReorderForm(); $form = new UserBackendReorderForm();
$form->setIniConfig(Config::app('authentication')); $form->setIniConfig(Config::app('authentication'));
$form->handleRequest(); $form->handleRequest();
$this->view->form = $form; $this->view->form = $form;
$this->view->tabs->activate('authentication'); $this->view->tabs->activate('userbackend');
$this->render('authentication/reorder'); $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'); $this->assertPermission('config/application/userbackend');
$form = new AuthenticationBackendConfigForm(); $form = new UserBackendConfigForm();
$form->setTitle($this->translate('Create New Authentication Backend')); $form->setTitle($this->translate('Create New User Backend'));
$form->addDescription($this->translate( $form->addDescription($this->translate(
'Create a new backend for authenticating your users. This backend' 'Create a new backend for authenticating your users. This backend'
. ' will be added at the end of your authentication order.' . ' will be added at the end of your authentication order.'
)); ));
$form->setIniConfig(Config::app('authentication')); $form->setIniConfig(Config::app('authentication'));
$form->setResourceConfig(ResourceFactory::getResourceConfigs()); $form->setResourceConfig(ResourceFactory::getResourceConfigs());
$form->setRedirectUrl('config/authentication'); $form->setRedirectUrl('config/userbackend');
$form->handleRequest(); $form->handleRequest();
$this->view->form = $form; $this->view->form = $form;
$this->view->tabs->activate('authentication'); $this->view->tabs->activate('userbackend');
$this->render('authentication/create'); $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'); $this->assertPermission('config/application/userbackend');
$form = new AuthenticationBackendConfigForm(); $form = new UserBackendConfigForm();
$form->setTitle($this->translate('Edit Backend')); $form->setTitle($this->translate('Edit User Backend'));
$form->setIniConfig(Config::app('authentication')); $form->setIniConfig(Config::app('authentication'));
$form->setResourceConfig(ResourceFactory::getResourceConfigs()); $form->setResourceConfig(ResourceFactory::getResourceConfigs());
$form->setRedirectUrl('config/authentication'); $form->setRedirectUrl('config/userbackend');
$form->setAction(Url::fromRequest());
$form->handleRequest(); $form->handleRequest();
$this->view->form = $form; $this->view->form = $form;
$this->view->tabs->activate('authentication'); $this->view->tabs->activate('userbackend');
$this->render('authentication/modify'); $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( $form = new ConfirmRemovalForm(array(
'onSuccess' => function ($form) { 'onSuccess' => function ($form) {
$configForm = new AuthenticationBackendConfigForm(); $configForm = new UserBackendConfigForm();
$configForm->setIniConfig(Config::app('authentication')); $configForm->setIniConfig(Config::app('authentication'));
$authBackend = $form->getRequest()->getQuery('auth_backend'); $authBackend = $form->getRequest()->getQuery('backend');
try { try {
$configForm->remove($authBackend); $configForm->remove($authBackend);
@ -276,7 +276,7 @@ class ConfigController extends Controller
if ($configForm->save()) { if ($configForm->save()) {
Notification::success(sprintf( Notification::success(sprintf(
t('Authentication backend "%s" has been successfully removed'), t('User backend "%s" has been successfully removed'),
$authBackend $authBackend
)); ));
} else { } else {
@ -284,13 +284,14 @@ class ConfigController extends Controller
} }
} }
)); ));
$form->setTitle($this->translate('Remove Backend')); $form->setTitle($this->translate('Remove User Backend'));
$form->setRedirectUrl('config/authentication'); $form->setRedirectUrl('config/userbackend');
$form->setAction(Url::fromRequest());
$form->handleRequest(); $form->handleRequest();
$this->view->form = $form; $this->view->form = $form;
$this->view->tabs->activate('authentication'); $this->view->tabs->activate('userbackend');
$this->render('authentication/remove'); $this->render('userbackend/remove');
} }
/** /**
@ -373,7 +374,7 @@ class ConfigController extends Controller
if ($config->get('resource') === $resource) { if ($config->get('resource') === $resource) {
$form->addDescription(sprintf( $form->addDescription(sprintf(
$this->translate( $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.' 'Removing the resource can result in noone being able to log in any longer.'
), ),
$resource, $resource,

View File

@ -0,0 +1,345 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Exception;
use Icinga\Application\Logger;
use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\Data\Reducible;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\Config\UserGroup\AddMemberForm;
use Icinga\Forms\Config\UserGroup\UserGroupForm;
use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\Form;
use Icinga\Web\Notification;
use Icinga\Web\Url;
use Icinga\Web\Widget;
class GroupController extends AuthBackendController
{
/**
* List all user groups of a single backend
*/
public function listAction()
{
$this->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;
}
}

View File

@ -4,69 +4,27 @@
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Forms\ConfirmRemovalForm; use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Forms\Security\RoleForm; use Icinga\Forms\Security\RoleForm;
use Icinga\Web\Controller\ActionController; use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\Notification; use Icinga\Web\Notification;
use Icinga\Web\Widget;
/** class RoleController extends AuthBackendController
* Roles configuration
*/
class RolesController extends ActionController
{ {
/**
* 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 * 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); $this->view->roles = Config::app('roles', true);
} }
/** /**
* Create a new role * Create a new role
*/ */
public function newAction() public function addAction()
{ {
$this->assertPermission('config/authentication/roles/add');
$role = new RoleForm(array( $role = new RoleForm(array(
'onSuccess' => function (RoleForm $role) { 'onSuccess' => function (RoleForm $role) {
$name = $role->getElement('name')->getValue(); $name = $role->getElement('name')->getValue();
@ -88,9 +46,10 @@ class RolesController extends ActionController
->setTitle($this->translate('New Role')) ->setTitle($this->translate('New Role'))
->setSubmitLabel($this->translate('Create Role')) ->setSubmitLabel($this->translate('Create Role'))
->setIniConfig(Config::app('roles', true)) ->setIniConfig(Config::app('roles', true))
->setRedirectUrl('roles') ->setRedirectUrl('role/list')
->handleRequest(); ->handleRequest();
$this->view->form = $role; $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 * @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'); $name = $this->_request->getParam('role');
if (empty($name)) { if (empty($name)) {
throw new Zend_Controller_Action_Exception( throw new Zend_Controller_Action_Exception(
@ -137,9 +97,10 @@ class RolesController extends ActionController
} }
return false; return false;
}) })
->setRedirectUrl('roles') ->setRedirectUrl('role/list')
->handleRequest(); ->handleRequest();
$this->view->form = $role; $this->view->form = $role;
$this->render('form');
} }
/** /**
@ -149,6 +110,7 @@ class RolesController extends ActionController
*/ */
public function removeAction() public function removeAction()
{ {
$this->assertPermission('config/authentication/roles/remove');
$name = $this->_request->getParam('role'); $name = $this->_request->getParam('role');
if (empty($name)) { if (empty($name)) {
throw new Zend_Controller_Action_Exception( throw new Zend_Controller_Action_Exception(
@ -185,8 +147,9 @@ class RolesController extends ActionController
$confirmation $confirmation
->setTitle(sprintf($this->translate('Remove Role %s'), $name)) ->setTitle(sprintf($this->translate('Remove Role %s'), $name))
->setSubmitLabel($this->translate('Remove Role')) ->setSubmitLabel($this->translate('Remove Role'))
->setRedirectUrl('roles') ->setRedirectUrl('role/list')
->handleRequest(); ->handleRequest();
$this->view->form = $confirmation; $this->view->form = $confirmation;
$this->render('form');
} }
} }

View File

@ -0,0 +1,304 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Exception;
use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\Config\User\CreateMembershipForm;
use Icinga\Forms\Config\User\UserForm;
use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\User;
use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\Form;
use Icinga\Web\Notification;
use Icinga\Web\Url;
use Icinga\Web\Widget;
class UserController extends AuthBackendController
{
/**
* List all users of a single backend
*/
public function listAction()
{
$this->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;
}
}

View File

@ -0,0 +1,183 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Exception;
use Icinga\Application\Config;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Forms\Config\UserGroup\UserGroupBackendForm;
use Icinga\Web\Controller;
use Icinga\Web\Notification;
use Icinga\Web\Url;
/**
* Controller to configure user group backends
*/
class UsergroupbackendController extends Controller
{
/**
* Initialize this controller
*/
public function init()
{
$this->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;
}
}

View File

@ -0,0 +1,191 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\User;
use Exception;
use Icinga\Application\Logger;
use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form for creating one or more group memberships
*/
class CreateMembershipForm extends Form
{
/**
* The user group backends to fetch groups from
*
* Each backend must implement the Icinga\Data\Extensible and Icinga\Data\Selectable interface.
*
* @var array
*/
protected $backends;
/**
* The username to create memberships for
*
* @var string
*/
protected $userName;
/**
* Set the user group backends to fetch groups from
*
* @param array $backends
*
* @return $this
*/
public function setBackends($backends)
{
$this->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);
}
}

View File

@ -0,0 +1,175 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\User;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\RepositoryForm;
class UserForm extends RepositoryForm
{
/**
* Create and add elements to this form to insert or update a user
*
* @param array $formData The data sent by the user
*/
protected function createInsertElements(array $formData)
{
$this->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());
}
}
}

View File

@ -1,16 +1,16 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\Authentication; namespace Icinga\Forms\Config\UserBackend;
use Exception; use Exception;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Authentication\Backend\DbUserBackend; use Icinga\Authentication\User\DbUserBackend;
/** /**
* Form class for adding/modifying database authentication backends * Form class for adding/modifying database user backends
*/ */
class DbBackendForm extends Form class DbBackendForm extends Form
{ {
@ -85,13 +85,13 @@ class DbBackendForm extends Form
} }
/** /**
* Validate that the selected resource is a valid database authentication backend * Validate that the selected resource is a valid database user backend
* *
* @see Form::onSuccess() * @see Form::onSuccess()
*/ */
public function onSuccess() public function onSuccess()
{ {
if (false === static::isValidAuthenticationBackend($this)) { if (false === static::isValidUserBackend($this)) {
return false; return false;
} }
} }
@ -103,12 +103,12 @@ class DbBackendForm extends Form
* *
* @return bool Whether validation succeeded or not * @return bool Whether validation succeeded or not
*/ */
public static function isValidAuthenticationBackend(Form $form) public static function isValidUserBackend(Form $form)
{ {
try { try {
$dbUserBackend = new DbUserBackend(ResourceFactory::createResource($form->getResourceConfig())); $dbUserBackend = new DbUserBackend(ResourceFactory::createResource($form->getResourceConfig()));
if ($dbUserBackend->count() < 1) { if ($dbUserBackend->select()->where('is_active', true)->count() < 1) {
$form->addError($form->translate('No users found under the specified database backend')); $form->addError($form->translate('No active users found under the specified database backend'));
return false; return false;
} }
} catch (Exception $e) { } catch (Exception $e) {

View File

@ -1,13 +1,13 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\Authentication; namespace Icinga\Forms\Config\UserBackend;
use Zend_Validate_Callback; use Zend_Validate_Callback;
use Icinga\Web\Form; use Icinga\Web\Form;
/** /**
* Form class for adding/modifying authentication backends of type "external" * Form class for adding/modifying user backends of type "external"
*/ */
class ExternalBackendForm extends Form class ExternalBackendForm extends Form
{ {
@ -90,7 +90,7 @@ class ExternalBackendForm extends Form
* *
* @return bool Whether validation succeeded or not * @return bool Whether validation succeeded or not
*/ */
public static function isValidAuthenticationBackend(Form $form) public static function isValidUserBackend(Form $form)
{ {
return true; return true;
} }

View File

@ -1,17 +1,17 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\Authentication; namespace Icinga\Forms\Config\UserBackend;
use Exception; use Exception;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Exception\AuthenticationException; use Icinga\Exception\AuthenticationException;
use Icinga\Authentication\Backend\LdapUserBackend; use Icinga\Authentication\User\LdapUserBackend;
/** /**
* Form class for adding/modifying LDAP authentication backends * Form class for adding/modifying LDAP user backends
*/ */
class LdapBackendForm extends Form class LdapBackendForm extends Form
{ {
@ -149,13 +149,13 @@ class LdapBackendForm extends Form
} }
/** /**
* Validate that the selected resource is a valid ldap authentication backend * Validate that the selected resource is a valid ldap user backend
* *
* @see Form::onSuccess() * @see Form::onSuccess()
*/ */
public function onSuccess() public function onSuccess()
{ {
if (false === static::isValidAuthenticationBackend($this)) { if (false === static::isValidUserBackend($this)) {
return false; return false;
} }
} }
@ -167,16 +167,11 @@ class LdapBackendForm extends Form
* *
* @return bool Whether validation succeeded or not * @return bool Whether validation succeeded or not
*/ */
public static function isValidAuthenticationBackend(Form $form) public static function isValidUserBackend(Form $form)
{ {
try { try {
$ldapUserBackend = new LdapUserBackend( $ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig()));
ResourceFactory::createResource($form->getResourceConfig()), $ldapUserBackend->setConfig(new ConfigObject($form->getValues()));
$form->getElement('user_class')->getValue(),
$form->getElement('user_name_attribute')->getValue(),
$form->getElement('base_dn')->getValue(),
$form->getElement('filter')->getValue()
);
$ldapUserBackend->assertAuthenticationPossible(); $ldapUserBackend->assertAuthenticationPossible();
} catch (AuthenticationException $e) { } catch (AuthenticationException $e) {
if (($previous = $e->getPrevious()) !== null) { if (($previous = $e->getPrevious()) !== null) {

View File

@ -11,11 +11,11 @@ use Icinga\Application\Platform;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Forms\Config\Authentication\DbBackendForm; use Icinga\Forms\Config\UserBackend\DbBackendForm;
use Icinga\Forms\Config\Authentication\LdapBackendForm; use Icinga\Forms\Config\UserBackend\LdapBackendForm;
use Icinga\Forms\Config\Authentication\ExternalBackendForm; use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
class AuthenticationBackendConfigForm extends ConfigForm class UserBackendConfigForm extends ConfigForm
{ {
/** /**
* The available resources split by type * 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'. * 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'] : ''; $name = isset($values['name']) ? $values['name'] : '';
if (! $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)) { } 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']); 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 string $name The name of the backend to edit
* @param array $values The values to edit the configuration with * @param array $values The values to edit the configuration with
@ -113,11 +113,11 @@ class AuthenticationBackendConfigForm extends ConfigForm
public function edit($name, array $values) public function edit($name, array $values)
{ {
if (! $name) { 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'] : '')) { } 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)) { } 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); $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 * @param string $name The name of the backend to remove
* *
@ -143,9 +143,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
public function remove($name) public function remove($name)
{ {
if (! $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)) { } 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); $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 string $name The name of the backend to be moved
* @param int $position The new (absolute) position of the backend * @param int $position The new (absolute) position of the backend
@ -166,9 +166,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
public function move($name, $position) public function move($name, $position)
{ {
if (! $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)) { } 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(); $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 * 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. * 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()) { if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
$backendForm = $this->getBackendForm($this->getElement('type')->getValue()); $backendForm = $this->getBackendForm($this->getElement('type')->getValue());
if (false === $backendForm::isValidAuthenticationBackend($this)) { if (false === $backendForm::isValidUserBackend($this)) {
$this->addElement($this->getForceCreationCheckbox()); $this->addElement($this->getForceCreationCheckbox());
return false; return false;
} }
} }
$authBackend = $this->request->getQuery('auth_backend'); $authBackend = $this->request->getQuery('backend');
try { try {
if ($authBackend === null) { // create new backend if ($authBackend === null) { // create new backend
$this->add($this->getValues()); $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 } else { // edit existing backend
$this->edit($authBackend, $this->getValues()); $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) { } catch (InvalidArgumentException $e) {
Notification::error($e->getMessage()); 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() * @see Form::onRequest()
* *
@ -233,12 +233,12 @@ class AuthenticationBackendConfigForm extends ConfigForm
*/ */
public function onRequest() public function onRequest()
{ {
$authBackend = $this->request->getQuery('auth_backend'); $authBackend = $this->request->getQuery('backend');
if ($authBackend !== null) { if ($authBackend !== null) {
if ($authBackend === '') { 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)) { } 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) { } elseif ($this->config->getSection($authBackend)->backend === null) {
throw new ConfigurationError( throw new ConfigurationError(
sprintf($this->translate('Backend "%s" has no `backend\' setting'), $authBackend) sprintf($this->translate('Backend "%s" has no `backend\' setting'), $authBackend)

View File

@ -7,7 +7,7 @@ use InvalidArgumentException;
use Icinga\Web\Notification; use Icinga\Web\Notification;
use Icinga\Forms\ConfigForm; use Icinga\Forms\ConfigForm;
class AuthenticationBackendReorderForm extends ConfigForm class UserBackendReorderForm extends ConfigForm
{ {
/** /**
* Initialize this form * 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() * @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 * @return ConfigForm
*/ */
protected function getConfigForm() protected function getConfigForm()
{ {
$form = new AuthenticationBackendConfigForm(); $form = new UserBackendConfigForm();
$form->setIniConfig($this->config); $form->setIniConfig($this->config);
return $form; return $form;
} }

View File

@ -0,0 +1,182 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Exception;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Selectable;
use Icinga\Exception\NotFoundError;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form for adding one or more group members
*/
class AddMemberForm extends Form
{
/**
* The data source to fetch users from
*
* @var Selectable
*/
protected $ds;
/**
* The user group backend to use
*
* @var Extensible
*/
protected $backend;
/**
* The group to add members for
*
* @var string
*/
protected $groupName;
/**
* Set the data source to fetch users from
*
* @param Selectable $ds
*
* @return $this
*/
public function setDataSource(Selectable $ds)
{
$this->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;
}
}

View File

@ -0,0 +1,58 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Icinga\Data\ResourceFactory;
use Icinga\Web\Form;
/**
* Form for managing database user group backends
*/
class DbUserGroupBackendForm extends Form
{
/**
* Initialize this form
*/
public function init()
{
$this->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;
}
}

View File

@ -0,0 +1,198 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use InvalidArgumentException;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\ConfigForm;
/**
* Form for managing user group backends
*/
class UserGroupBackendForm extends ConfigForm
{
/**
* Initialize this form
*/
public function init()
{
$this->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());
}
}

View File

@ -0,0 +1,126 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\RepositoryForm;
class UserGroupForm extends RepositoryForm
{
/**
* Create and add elements to this form to insert or update a group
*
* @param array $formData The data sent by the user
*/
protected function createInsertElements(array $formData)
{
$this->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());
}
}
}

View File

@ -0,0 +1,389 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms;
use Exception;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotFoundError;
use Icinga\Repository\Repository;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form base-class providing standard functionality for extensible, updatable and reducible repositories
*/
abstract class RepositoryForm extends Form
{
/**
* Insert mode
*/
const MODE_INSERT = 0;
/**
* Update mode
*/
const MODE_UPDATE = 1;
/**
* Delete mode
*/
const MODE_DELETE = 2;
/**
* The repository being worked with
*
* @var Repository
*/
protected $repository;
/**
* How to interact with the repository
*
* @var int
*/
protected $mode;
/**
* The name of the entry being handled when in mode update or delete
*
* @var string
*/
protected $identifier;
/**
* The data of the entry to pre-populate the form with when in mode insert or update
*
* @var type
*/
protected $data;
/**
* Set the repository to work with
*
* @param Repository $repository
*
* @return $this
*/
public function setRepository(Repository $repository)
{
$this->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);
}

View File

@ -21,14 +21,30 @@ class RoleForm extends ConfigForm
* @var array * @var array
*/ */
protected $providedPermissions = array( protected $providedPermissions = array(
'*' => '*', '*' => '*',
'config/*' => 'config/*', 'config/*' => 'config/*',
'config/application/*' => 'config/application/*', 'config/application/*' => 'config/application/*',
'config/application/general' => 'config/application/general', 'config/application/general' => 'config/application/general',
'config/application/authentication' => 'config/application/authentication', 'config/application/resources' => 'config/application/resources',
'config/application/resources' => 'config/application/resources', 'config/application/userbackend' => 'config/application/userbackend',
'config/application/roles' => 'config/application/roles', 'config/application/usergroupbackend' => 'config/application/usergroupbackend',
'config/modules' => 'config/modules' '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'
); );
/** /**

View File

@ -2,8 +2,8 @@
<?= $tabs; ?> <?= $tabs; ?>
</div> </div>
<div class="content" data-base-target="_next"> <div class="content" data-base-target="_next">
<a href="<?= $this->href('/config/createAuthenticationBackend'); ?>"> <a href="<?= $this->href('/config/createuserbackend'); ?>">
<?= $this->icon('plus'); ?><?= $this->translate('Create A New Authentication Backend'); ?> <?= $this->icon('plus'); ?><?= $this->translate('Create A New User Backend'); ?>
</a> </a>
<div id="authentication-reorder-form"> <div id="authentication-reorder-form">
<?= $form; ?> <?= $form; ?>

View File

@ -12,22 +12,22 @@
<td class="action"> <td class="action">
<?= $this->qlink( <?= $this->qlink(
$backendNames[$i], $backendNames[$i],
'config/editAuthenticationBackend', 'config/edituserbackend',
array('auth_backend' => $backendNames[$i]), array('backend' => $backendNames[$i]),
array( array(
'icon' => 'edit', 'icon' => 'edit',
'title' => sprintf($this->translate('Edit authentication backend %s'), $backendNames[$i]) 'title' => sprintf($this->translate('Edit user backend %s'), $backendNames[$i])
) )
); ?> ); ?>
</td> </td>
<td> <td>
<?= $this->qlink( <?= $this->qlink(
'', '',
'config/removeAuthenticationBackend', 'config/removeuserbackend',
array('auth_backend' => $backendNames[$i]), array('backend' => $backendNames[$i]),
array( array(
'icon' => 'trash', 'icon' => 'trash',
'title' => sprintf($this->translate('Remove authentication backend %s'), $backendNames[$i]) 'title' => sprintf($this->translate('Remove user backend %s'), $backendNames[$i])
) )
); ?> ); ?>
</td> </td>
@ -40,7 +40,7 @@
); ?>" title="<?= $this->translate( ); ?>" title="<?= $this->translate(
'Move up in authentication order' 'Move up in authentication order'
); ?>" aria-label="<?= sprintf( ); ?>" aria-label="<?= sprintf(
$this->translate('Move authentication backend %s upwards'), $this->translate('Move user backend %s upwards'),
$backendNames[$i] $backendNames[$i]
); ?>"> ); ?>">
<?= $this->icon('up-big'); ?> <?= $this->icon('up-big'); ?>
@ -54,7 +54,7 @@
); ?>" title="<?= $this->translate( ); ?>" title="<?= $this->translate(
'Move down in authentication order' 'Move down in authentication order'
); ?>" aria-label="<?= sprintf( ); ?>" aria-label="<?= sprintf(
$this->translate('Move authentication backend %s downwards'), $this->translate('Move user backend %s downwards'),
$backendNames[$i] $backendNames[$i]
); ?>"> ); ?>">
<?= $this->icon('down-big'); ?> <?= $this->icon('down-big'); ?>

View File

@ -1,6 +1,6 @@
<div class="controls"> <div class="controls">
<?= $tabs->showOnlyCloseButton() ?> <?= $tabs->showOnlyCloseButton(); ?>
</div> </div>
<div class="content"> <div class="content">
<?= $form ?> <?= $form; ?>
</div> </div>

View File

@ -0,0 +1,78 @@
<?php
use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<div>
<?= $this->backendSelection; ?>
<?= $this->filterEditor; ?>
</div>
</div>
<?php endif ?>
<div class="content groups">
<?php
if ($backend === null) {
echo $this->translate('No backend found which is able to list groups') . '</div>';
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): ?>
<table data-base-target="_next" class="action group-list">
<thead>
<tr>
<th class="group-name"><?= $this->translate('Group'); ?></th>
<?php if ($reducible): ?>
<th class="group-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td class="group-name"><?= $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)
)); ?></td>
<?php if ($reducible): ?>
<td class="group-remove">
<?= $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'
)
); ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No groups found matching the filter'); ?></p>
<?php endif ?>
<?php if ($extensible): ?>
<?= $this->qlink($this->translate('Add a new group'), 'group/add', array('backend' => $backend->getName()), array(
'icon' => 'plus',
'data-base-target' => '_next',
'class' => 'group-add'
)); ?>
<?php endif ?>
</div>

View File

@ -0,0 +1,81 @@
<?php
use Icinga\Data\Extensible;
use Icinga\Data\Updatable;
$extensible = $this->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'
)
);
}
?>
<div class="controls">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?php endif ?>
<div class="group-header">
<p class="group-name"><strong><?= $this->escape($group->group_name); ?></strong></p> <?= $editLink; ?>
<p class="group-created"><strong><?= $this->translate('Created at'); ?>:</strong> <?= $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?></p>
<p class="group-modified"><strong><?= $this->translate('Last modified'); ?>:</strong> <?= $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?></p>
</div>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content members" data-base-target="_next">
<?php if (count($members) > 0): ?>
<table data-base-target="_next" class="action member-list">
<thead>
<tr>
<th class="member-name"><?= $this->translate('Username'); ?></th>
<?php if (isset($removeForm)): ?>
<th class="member-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($members as $member): ?>
<tr>
<td class="member-name"><?= $this->escape($member->user_name); ?></td>
<?php if (isset($removeForm)): ?>
<td class="member-remove" data-base-target="_self">
<?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No group member found matching the filter'); ?></p>
<?php endif ?>
<?php if ($extensible): ?>
<?= $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'
)); ?>
<?php endif ?>
</div>

View File

@ -1,6 +1,6 @@
<div class="controls"> <div class="controls">
<?= $tabs->showOnlyCloseButton() ?> <?= $tabs->showOnlyCloseButton(); ?>
</div> </div>
<div class="content"> <div class="content">
<?= $form ?> <?= $form; ?>
</div> </div>

View File

@ -22,7 +22,7 @@
<td> <td>
<?= $this->qlink( <?= $this->qlink(
$name, $name,
'roles/update', 'role/edit',
array('role' => $name), array('role' => $name),
array('title' => sprintf($this->translate('Edit role %s'), $name)) array('title' => sprintf($this->translate('Edit role %s'), $name))
); ?> ); ?>
@ -54,7 +54,7 @@
<td> <td>
<?= $this->qlink( <?= $this->qlink(
'', '',
'roles/remove', 'role/remove',
array('role' => $name), array('role' => $name),
array( array(
'icon' => 'trash', 'icon' => 'trash',
@ -67,7 +67,7 @@
</tbody> </tbody>
</table> </table>
<?php endif ?> <?php endif ?>
<a data-base-target="_next" href="<?= $this->href('roles/new') ?>"> <a data-base-target="_next" href="<?= $this->href('role/new') ?>">
<?= $this->translate('Create a New Role') ?> <?= $this->translate('Create a New Role') ?>
</a> </a>
</div> </div>

View File

@ -1,6 +1,6 @@
<div class="controls"> <div class="controls">
<?= $tabs->showOnlyCloseButton() ?> <?= $tabs->showOnlyCloseButton(); ?>
</div> </div>
<div class="content"> <div class="content">
<?= $form ?> <?= $form; ?>
</div> </div>

View File

@ -0,0 +1,78 @@
<?php
use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<div>
<?= $this->backendSelection; ?>
<?= $this->filterEditor; ?>
</div>
</div>
<?php endif ?>
<div class="content users">
<?php
if ($backend === null) {
echo $this->translate('No backend found which is able to list users') . '</div>';
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): ?>
<table data-base-target="_next" class="action user-list">
<thead>
<tr>
<th class="user-name"><?= $this->translate('Username'); ?></th>
<?php if ($reducible): ?>
<th class="user-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td class="user-name"><?= $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)
)); ?></td>
<?php if ($reducible): ?>
<td class="user-remove">
<?= $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'
)
); ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No users found matching the filter'); ?></p>
<?php endif ?>
<?php if ($extensible): ?>
<?= $this->qlink($this->translate('Add a new user'), 'user/add', array('backend' => $backend->getName()), array(
'icon' => 'plus',
'data-base-target' => '_next',
'class' => 'user-add'
)); ?>
<?php endif ?>
</div>

View File

@ -0,0 +1,95 @@
<?php
use Icinga\Data\Updatable;
use Icinga\Data\Reducible;
use Icinga\Data\Selectable;
$editLink = null;
if ($this->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'
)
);
}
?>
<div class="controls">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?php endif ?>
<div class="user-header">
<p class="user-name"><strong><?= $this->escape($user->user_name); ?></strong></p> <?= $editLink; ?>
<p class="user-state"><strong><?= $this->translate('State'); ?>:</strong> <?= $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?></p>
<p class="user-created"><strong><?= $this->translate('Created at'); ?>:</strong> <?= $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?></p>
<p class="user-modified"><strong><?= $this->translate('Last modified'); ?>:</strong> <?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></p>
</div>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content memberships" data-base-target="_next">
<?php if (count($memberships) > 0): ?>
<table data-base-target="_next" class="action membership-list">
<thead>
<tr>
<th class="membership-group"><?= $this->translate('Group'); ?></th>
<th class="membership-cancel"><?= $this->translate('Cancel', 'group.membership'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($memberships as $membership): ?>
<tr>
<td class="membership-group">
<?php if ($this->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)
)); ?>
<?php else: ?>
<?= $this->escape($membership->group_name); ?>
<?php endif ?>
</td>
<td class="membership-cancel" data-base-target="_self">
<?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?>
<?= $removeForm->setAction($this->url('group/removemember', array(
'backend' => $membership->backend->getName(),
'group' => $membership->group_name
))); ?>
<?php else: ?>
-
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No memberships found matching the filter'); ?></p>
<?php endif ?>
<?php if ($showCreateMembershipLink): ?>
<?= $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'
)); ?>
<?php endif ?>
</div>

View File

@ -0,0 +1,6 @@
<div class="controls">
<?= $tabs->showOnlyCloseButton(); ?>
</div>
<div class="content">
<?= $form; ?>
</div>

View File

@ -0,0 +1,46 @@
<div class="controls">
<?= $tabs; ?>
</div>
<div class="content" data-base-target="_next">
<?= $this->qlink(
$this->translate('Create A New User Group Backend'),
'usergroupbackend/create',
null,
array(
'icon' => 'plus'
)
); ?>
<?php if (count($backendNames) > 0): ?>
<table class="action usergroupbackend-list">
<thead>
<tr>
<th class="backend-name"><?= $this->translate('Backend'); ?></th>
<th class="backend-remove"><?= $this->translate('Remove'); ?></th>
<tr>
</thead>
<tbody>
<?php foreach ($backendNames as $backendName): ?>
<tr>
<td class="backend-name">
<?= $this->qlink(
$backendName,
'usergroupbackend/edit',
array('backend' => $backendName),
array('title' => sprintf($this->translate('Edit user group backend %s'), $backendName))
); ?>
</td>
<td class="backend-remove"><?= $this->qlink(
null,
'usergroupbackend/remove',
array('backend' => $backendName),
array(
'title' => sprintf($this->translate('Remove user group backend %s'), $backendName),
'icon' => 'trash'
)
); ?></td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php endif ?>
</div>

View File

@ -1,21 +1,25 @@
# Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ # Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+
CREATE TABLE `icingaweb_group`( CREATE TABLE `icingaweb_group`(
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL, `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, `ctime` timestamp NULL DEFAULT NULL,
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `icingaweb_group_membership`( 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, `username` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`ctime` timestamp NULL DEFAULT NULL, `ctime` timestamp NULL DEFAULT NULL,
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`group_name`,`username`), PRIMARY KEY (`group_id`,`username`),
CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_name`) CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_id`)
REFERENCES `icingaweb_group` (`name`) REFERENCES `icingaweb_group` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `icingaweb_user`( CREATE TABLE `icingaweb_user`(

View File

@ -1,8 +1,13 @@
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* 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" ( CREATE TABLE "icingaweb_group" (
"id" serial,
"name" character varying(64) NOT NULL, "name" character varying(64) NOT NULL,
"parent" character varying(64) NULL DEFAULT NULL, "parent" int NULL DEFAULT NULL,
"ctime" timestamp NULL DEFAULT NULL, "ctime" timestamp NULL DEFAULT NULL,
"mtime" timestamp NULL DEFAULT NULL "mtime" timestamp NULL DEFAULT NULL
); );
@ -10,7 +15,7 @@ CREATE TABLE "icingaweb_group" (
ALTER TABLE ONLY "icingaweb_group" ALTER TABLE ONLY "icingaweb_group"
ADD CONSTRAINT pk_icingaweb_group ADD CONSTRAINT pk_icingaweb_group
PRIMARY KEY ( PRIMARY KEY (
"name" "id"
); );
CREATE UNIQUE INDEX idx_icingaweb_group CREATE UNIQUE INDEX idx_icingaweb_group
@ -19,8 +24,17 @@ CREATE UNIQUE INDEX idx_icingaweb_group
lower((name)::text) 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" ( CREATE TABLE "icingaweb_group_membership" (
"group_name" character varying(64) NOT NULL, "group_id" int NOT NULL,
"username" character varying(64) NOT NULL, "username" character varying(64) NOT NULL,
"ctime" timestamp NULL DEFAULT NULL, "ctime" timestamp NULL DEFAULT NULL,
"mtime" timestamp NULL DEFAULT NULL "mtime" timestamp NULL DEFAULT NULL
@ -28,15 +42,17 @@ CREATE TABLE "icingaweb_group_membership" (
ALTER TABLE ONLY "icingaweb_group_membership" ALTER TABLE ONLY "icingaweb_group_membership"
ADD CONSTRAINT pk_icingaweb_group_membership ADD CONSTRAINT pk_icingaweb_group_membership
PRIMARY KEY ( FOREIGN KEY (
"group_name", "group_id"
"username" )
REFERENCES "icingaweb_group" (
"id"
); );
CREATE UNIQUE INDEX idx_icingaweb_group_membership CREATE UNIQUE INDEX idx_icingaweb_group_membership
ON "icingaweb_group_membership" ON "icingaweb_group_membership"
USING btree ( USING btree (
lower((group_name)::text), group_id,
lower((username)::text) lower((username)::text)
); );

View File

@ -9,13 +9,15 @@ use LogicException;
use UnexpectedValueException; use UnexpectedValueException;
use Icinga\Util\File; use Icinga\Util\File;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\Selectable;
use Icinga\Data\SimpleQuery;
use Icinga\File\Ini\IniWriter; use Icinga\File\Ini\IniWriter;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;
/** /**
* Container for INI like configuration and global registry of application and module related configuration. * 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 * Configuration directory where ALL (application and module) configuration is located
@ -85,6 +87,26 @@ class Config implements Countable, Iterator
return $this; 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 * Return the count of available sections
* *
@ -92,7 +114,7 @@ class Config implements Countable, Iterator
*/ */
public function count() public function count()
{ {
return $this->config->count(); return $this->select()->count();
} }
/** /**

View File

@ -243,7 +243,9 @@ class Logger
return vsprintf( return vsprintf(
array_shift($arguments), array_shift($arguments),
array_map( 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 $arguments
) )
); );

View File

@ -179,10 +179,26 @@ class Module
protected $paneItems = array(); protected $paneItems = array();
/** /**
* A set of objects representing a searchUrl configuration
*
* @var array * @var array
*/ */
protected $searchUrls = 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 * Provide a search URL
* *
@ -201,6 +217,11 @@ class Module
$this->searchUrls[] = $searchUrl; $this->searchUrls[] = $searchUrl;
} }
/**
* Return this module's search urls
*
* @return array
*/
public function getSearchUrls() public function getSearchUrls()
{ {
$this->launchConfigScript(); $this->launchConfigScript();
@ -702,6 +723,28 @@ class Module
return new $this->setupWizard; 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 * Provide a named permission
* *
@ -777,6 +820,34 @@ class Module
return $this; 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 * Register new namespaces on the autoloader
* *

View File

@ -5,6 +5,8 @@ namespace Icinga\Authentication;
use Iterator; use Iterator;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\UserBackendInterface;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
@ -24,7 +26,7 @@ class AuthChain implements Iterator
/** /**
* The consecutive user backend while looping * The consecutive user backend while looping
* *
* @var UserBackend * @var UserBackendInterface
*/ */
private $currentBackend; private $currentBackend;
@ -52,7 +54,7 @@ class AuthChain implements Iterator
/** /**
* Return the current user backend * Return the current user backend
* *
* @return UserBackend * @return UserBackendInterface
*/ */
public function current() public function current()
{ {

View File

@ -1,206 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use PDO;
use Icinga\Authentication\UserBackend;
use Icinga\Data\Db\DbConnection;
use Icinga\User;
use Icinga\Exception\AuthenticationException;
use Exception;
use Zend_Db_Expr;
use Zend_Db_Select;
class DbUserBackend extends UserBackend
{
/**
* The algorithm to use when hashing passwords
*
* @var string
*/
const HASH_ALGORITHM = '$1$'; // MD5
/**
* The length of the salt to use when hashing a password
*
* @var int
*/
const SALT_LENGTH = 12; // 12 is required by MD5
/**
* Connection to the database
*
* @var DbConnection
*/
protected $conn;
public function __construct(DbConnection $conn)
{
$this->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;
}
}

View File

@ -1,62 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use Icinga\Authentication\UserGroupBackend;
use Icinga\Data\Db\DbConnection;
use Icinga\User;
/**
* Database user group backend
*/
class DbUserGroupBackend extends UserGroupBackend
{
/**
* Connection to the database
*
* @var DbConnection
*/
private $conn;
/**
* Create a new database user group backend
*
* @param DbConnection $conn
*/
public function __construct(DbConnection $conn)
{
$this->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;
}
}

View File

@ -1,64 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use Icinga\Application\Config;
use Icinga\Authentication\UserGroupBackend;
use Icinga\Exception\ConfigurationError;
use Icinga\User;
use Icinga\Util\String;
/**
* INI user group backend
*/
class IniUserGroupBackend extends UserGroupBackend
{
/**
* Config
*
* @var Config
*/
private $config;
/**
* Create a new INI user group backend
*
* @param Config $config
*/
public function __construct(Config $config)
{
$this->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;
}
}

View File

@ -1,291 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use Icinga\User;
use Icinga\Authentication\UserBackend;
use Icinga\Protocol\Ldap\Query;
use Icinga\Protocol\Ldap\Connection;
use Icinga\Exception\AuthenticationException;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression;
class LdapUserBackend extends UserBackend
{
/**
* Connection to the LDAP server
*
* @var Connection
*/
protected $conn;
protected $baseDn;
protected $userClass;
protected $userNameAttribute;
protected $customFilter;
protected $groupOptions;
/**
* Normed attribute names based on known LDAP environments
*
* @var array
*/
protected $normedAttributes = array(
'uid' => '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:
* <ul>
* <li>Connection credentials are correct and the bind is possible</li>
* <li>At least one user exists</li>
* <li>The specified userClass has the property specified by userNameAttribute</li>
* </ul>
*
* @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;
}
}

View File

@ -4,6 +4,7 @@
namespace Icinga\Authentication; namespace Icinga\Authentication;
use Exception; use Exception;
use Icinga\Authentication\UserGroup\UserGroupBackend;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Exception\IcingaException; use Icinga\Exception\IcingaException;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;
@ -55,7 +56,7 @@ class Manager
} catch (NotReadableError $e) { } catch (NotReadableError $e) {
Logger::error( Logger::error(
new IcingaException( new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown', 'Cannot load preferences for user "%s". An exception was thrown: %s',
$username, $username,
$e $e
) )
@ -73,7 +74,7 @@ class Manager
} catch (Exception $e) { } catch (Exception $e) {
Logger::error( Logger::error(
new IcingaException( new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown', 'Cannot load preferences for user "%s". An exception was thrown: %s',
$username, $username,
$e $e
) )
@ -91,7 +92,7 @@ class Manager
$groupsFromBackend = $groupBackend->getMemberships($user); $groupsFromBackend = $groupBackend->getMemberships($user);
} catch (Exception $e) { } catch (Exception $e) {
Logger::error( 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, $username,
$name, $name,
$e $e

View File

@ -0,0 +1,249 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use Exception;
use PDO;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\AuthenticationException;
use Icinga\Repository\DbRepository;
use Icinga\User;
class DbUserBackend extends DbRepository implements UserBackendInterface
{
/**
* The algorithm to use when hashing passwords
*
* @var string
*/
const HASH_ALGORITHM = '$1$'; // MD5
/**
* The length of the salt to use when hashing a password
*
* @var int
*/
const SALT_LENGTH = 12; // 12 is required by MD5
/**
* The query columns being provided
*
* @var array
*/
protected $queryColumns = array(
'user' => 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()));
}
}

View File

@ -1,23 +1,29 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend; namespace Icinga\Authentication\User;
use Icinga\Authentication\UserBackend;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\User; use Icinga\User;
/** /**
* Test login with external authentication mechanism, e.g. Apache * Test login with external authentication mechanism, e.g. Apache
*/ */
class ExternalBackend extends UserBackend class ExternalBackend implements UserBackendInterface
{ {
/**
* The name of this backend
*
* @var string
*/
protected $name;
/** /**
* Regexp expression to strip values from a username * Regexp expression to strip values from a username
* *
* @var string * @var string
*/ */
private $stripUsernameRegexp; protected $stripUsernameRegexp;
/** /**
* Create new authentication backend of type "external" * Create new authentication backend of type "external"
@ -30,29 +36,44 @@ class ExternalBackend extends UserBackend
} }
/** /**
* Count the available users * Set this backend's name
* *
* Authenticaton backends of type "external" will always return 1 * @param string $name
* *
* @return int * @return $this
*/ */
public function count() public function setName($name)
{ {
return 1; $this->name = $name;
return $this;
} }
/** /**
* Test whether the given user exists * Return this backend's name
* *
* @param User $user * @return string
*
* @return bool
*/ */
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'])) { if (isset($_SERVER['REMOTE_USER'])) {
$username = $_SERVER['REMOTE_USER']; $username = $_SERVER['REMOTE_USER'];
$user->setRemoteUserInformation($username, 'REMOTE_USER'); $user->setRemoteUserInformation($username, 'REMOTE_USER');
if ($this->stripUsernameRegexp) { if ($this->stripUsernameRegexp) {
$stripped = preg_replace($this->stripUsernameRegexp, '', $username); $stripped = preg_replace($this->stripUsernameRegexp, '', $username);
if ($stripped !== false) { if ($stripped !== false) {
@ -61,23 +82,11 @@ class ExternalBackend extends UserBackend
$username = $stripped; $username = $stripped;
} }
} }
$user->setUsername($username); $user->setUsername($username);
return true; return true;
} }
return false; return false;
} }
/**
* Authenticate
*
* @param User $user
* @param string $password
*
* @return bool
*/
public function authenticate(User $user, $password = null)
{
return $this->hasUser($user);
}
} }

View File

@ -0,0 +1,493 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use DateTime;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ProgrammingError;
use Icinga\Repository\Repository;
use Icinga\Repository\RepositoryQuery;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression;
use Icinga\User;
class LdapUserBackend extends Repository implements UserBackendInterface
{
/**
* The base DN to use for a query
*
* @var string
*/
protected $baseDn;
/**
* The objectClass where look for users
*
* @var string
*/
protected $userClass;
/**
* The attribute name where to find a user's name
*
* @var string
*/
protected $userNameAttribute;
/**
* The custom LDAP filter to apply on search queries
*
* @var string
*/
protected $filter;
/**
* 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'
)
)
);
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:
* <ul>
* <li>Connection credentials are correct and the bind is possible</li>
* <li>At least one user exists</li>
* <li>The specified userClass has the property specified by userNameAttribute</li>
* </ul>
*
* @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
);
}
}
}

View File

@ -0,0 +1,193 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use Icinga\Application\Logger;
use Icinga\Application\Icinga;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
/**
* Factory for user backends
*/
class UserBackend
{
/**
* The default user backend types provided by Icinga Web 2
*
* @var array
*/
protected static $defaultBackends = array(
'external',
'db',
'ldap',
'msldap'
);
/**
* The registered custom user backends with their identifier as key and class name as value
*
* @var array
*/
protected static $customBackends;
/**
* Register all custom user backends from all loaded modules
*/
protected static function registerCustomUserBackends()
{
if (static::$customBackends !== null) {
return;
}
static::$customBackends = array();
$providedBy = array();
foreach (Icinga::app()->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;
}
}

View File

@ -0,0 +1,41 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use Icinga\Exception\AuthenticationException;
use Icinga\User;
/**
* Interface for user backends
*/
interface UserBackendInterface
{
/**
* Set this backend's name
*
* @param string $name
*
* @return $this
*/
public function setName($name);
/**
* Return this backend's name
*
* @return string
*/
public function getName();
/**
* 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);
}

View File

@ -1,168 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication;
use Countable;
use Icinga\Authentication\Backend\ExternalBackend;
use Icinga\Authentication\Backend\DbUserBackend;
use Icinga\Authentication\Backend\LdapUserBackend;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
use Icinga\User;
abstract class UserBackend implements Countable
{
/**
* Name of the backend
*
* @var string
*/
protected $name;
/**
* Setter for the backend's name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->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);
}

View File

@ -0,0 +1,264 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotFoundError;
use Icinga\Repository\DbRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\User;
class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterface
{
/**
* The query columns being provided
*
* @var array
*/
protected $queryColumns = array(
'group' => 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;
}
}

View File

@ -0,0 +1,121 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Exception\StatementException;
use Icinga\Data\Filter\Filter;
use Icinga\Repository\IniRepository;
use Icinga\User;
use Icinga\Util\String;
class IniUserGroupBackend extends IniRepository implements UserGroupBackendInterface
{
/**
* The query columns being provided
*
* @var array
*/
protected $queryColumns = array(
'groups' => 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;
}
}

View File

@ -0,0 +1,164 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Application\Logger;
use Icinga\Application\Icinga;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
/**
* Factory for user group backends
*/
class UserGroupBackend
{
/**
* The default user group backend types provided by Icinga Web 2
*
* @var array
*/
protected static $defaultBackends = array(
'db',
//'ini'
);
/**
* The registered custom user group backends with their identifier as key and class name as value
*
* @var array
*/
protected static $customBackends;
/**
* Register all custom user group backends from all loaded modules
*/
public static function registerCustomUserGroupBackends()
{
if (static::$customBackends !== null) {
return;
}
static::$customBackends = array();
$providedBy = array();
foreach (Icinga::app()->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;
}
}

View File

@ -0,0 +1,37 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\User;
/**
* Interface for user group backends
*/
interface UserGroupBackendInterface
{
/**
* Set this backend's name
*
* @param string $name
*
* @return $this
*/
public function setName($name);
/**
* Return this backend's name
*
* @return string
*/
public function getName();
/**
* Return the groups the given user is a member of
*
* @param User $user
*
* @return array
*/
public function getMemberships(User $user);
}

View File

@ -1,112 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication;
use Icinga\Authentication\Backend\DbUserGroupBackend;
use Icinga\Authentication\Backend\IniUserGroupBackend;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\IcingaException;
use Icinga\User;
/**
* Base class and factory for user group backends
*/
abstract class UserGroupBackend
{
/**
* Name of the backend
*
* @var string
*/
protected $name;
/**
* Set the backend name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->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);
}

View File

@ -4,22 +4,15 @@
namespace Icinga\Data; namespace Icinga\Data;
use Iterator; use Iterator;
use Countable;
use ArrayAccess; use ArrayAccess;
use LogicException; use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\Exception\ProgrammingError;
/** /**
* Container for configuration values * 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 * Create a new config
* *
@ -27,15 +20,14 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
*/ */
public function __construct(array $data = array()) public function __construct(array $data = array())
{ {
$this->data = array(); // Convert all embedded arrays to ConfigObjects as well
foreach ($data as & $value) {
foreach ($data as $key => $value) {
if (is_array($value)) { if (is_array($value)) {
$this->data[$key] = new static($value); $value = new static($value);
} else {
$this->data[$key] = $value;
} }
} }
parent::__construct($data);
} }
/** /**
@ -55,16 +47,6 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
$this->data = $array; $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 * Reset the current position of $this->data
* *
@ -197,7 +179,7 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
public function offsetSet($key, $value) public function offsetSet($key, $value)
{ {
if ($key === null) { 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; $this->$key = $value;

View File

@ -3,35 +3,105 @@
namespace Icinga\Data\DataArray; namespace Icinga\Data\DataArray;
use ArrayIterator;
use Icinga\Data\Selectable; use Icinga\Data\Selectable;
use Icinga\Data\SimpleQuery; use Icinga\Data\SimpleQuery;
class ArrayDatasource implements Selectable class ArrayDatasource implements Selectable
{ {
/**
* The array being used as data source
*
* @var array
*/
protected $data; protected $data;
/**
* The current result
*
* @var array
*/
protected $result; 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() public function select()
{ {
return new SimpleQuery($this); 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) public function fetchColumn(SimpleQuery $query)
{ {
$result = array(); $result = array();
@ -39,9 +109,17 @@ class ArrayDatasource implements Selectable
$arr = (array) $row; $arr = (array) $row;
$result[] = array_shift($arr); $result[] = array_shift($arr);
} }
return $result; 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) public function fetchPairs(SimpleQuery $query)
{ {
$result = array(); $result = array();
@ -53,104 +131,164 @@ class ArrayDatasource implements Selectable
$keys[1] = $keys[0]; $keys[1] = $keys[0];
} }
} }
$result[$row->{$keys[0]}] = $row->{$keys[1]}; $result[$row->{$keys[0]}] = $row->{$keys[1]};
} }
return $result; 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) public function fetchRow(SimpleQuery $query)
{ {
$result = $this->getResult($query); $result = $this->getResult($query);
if (empty($result)) { if (empty($result)) {
return false; 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) public function fetchAll(SimpleQuery $query)
{ {
return $this->getResult($query); return $this->getResult($query);
} }
/**
* Count all rows of the given query's result
*
* @param SimpleQuery $query
*
* @return int
*/
public function count(SimpleQuery $query) public function count(SimpleQuery $query)
{ {
$this->createResult($query); if ($this->count === null) {
return count($this->result); $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) protected function createResult(SimpleQuery $query)
{ {
if ($this->hasResult()) {
return $this;
}
$result = array();
$columns = $query->getColumns(); $columns = $query->getColumns();
$filter = $query->getFilter(); $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)) { if (! $filter->matches($row)) {
continue; continue;
} elseif ($skipped < $offset) {
$skipped++;
continue;
} }
// Get only desired columns if asked so // Get only desired columns if asked so
if (empty($columns)) { if (! empty($columns)) {
$result[] = $row; $filteredRow = (object) array();
} else { foreach ($columns as $alias => $name) {
$c_row = (object) array(); if (! is_string($alias)) {
foreach ($columns as $alias => $key) { $alias = $name;
if (is_int($alias)) {
$alias = $key;
} }
if (isset($row->$key)) {
$c_row->$alias = $row->$key; if (isset($row->$name)) {
$filteredRow->$alias = $row->$name;
} else { } 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 // Sort the result
if ($query->hasOrder()) { if ($query->hasOrder()) {
usort($result, array($query, 'compare')); if ($foundStringKey) {
} uasort($result, array($query, 'compare'));
$this->setResult($result);
return $this;
}
protected function getLimitedResult($query)
{
if ($query->hasLimit()) {
if ($query->hasOffset()) {
$offset = $query->getOffset();
} else { } else {
$offset = 0; usort($result, array($query, 'compare'));
} }
return array_slice($this->result, $offset, $query->getLimit()); } elseif (! $foundStringKey) {
} else { $result = array_values($result);
return $this->result;
} }
return $result;
} }
/**
* Return whether a query result exists
*
* @return bool
*/
protected function hasResult() protected function hasResult()
{ {
return $this->result !== null; 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) protected function getResult(SimpleQuery $query)
{ {
if (! $this->hasResult()) { if (! $this->hasResult()) {
$this->createResult($query); $this->setResult($this->createResult($query));
} }
return $this->getLimitedResult($query);
return $this->result;
} }
} }

View File

@ -4,18 +4,26 @@
namespace Icinga\Data\Db; namespace Icinga\Data\Db;
use PDO; use PDO;
use Iterator;
use Zend_Db; use Zend_Db;
use Icinga\Application\Benchmark;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\Db\DbQuery; 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\ResourceFactory;
use Icinga\Data\Selectable; use Icinga\Data\Selectable;
use Icinga\Data\Updatable;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Exception\ProgrammingError;
/** /**
* Encapsulate database connections and query creation * Encapsulate database connections and query creation
*/ */
class DbConnection implements Selectable class DbConnection implements Selectable, Extensible, Updatable, Reducible
{ {
/** /**
* Connection config * Connection config
@ -72,13 +80,25 @@ class DbConnection implements Selectable
/** /**
* Provide a query on this connection * Provide a query on this connection
* *
* @return Query * @return DbQuery
*/ */
public function select() public function select()
{ {
return new DbQuery($this); 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 * Getter for database type
* *
@ -191,6 +211,18 @@ class DbConnection implements Selectable
return $this; 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 * Retrieve an array containing all rows of the result set
* *
@ -200,10 +232,7 @@ class DbConnection implements Selectable
*/ */
public function fetchAll(DbQuery $query) public function fetchAll(DbQuery $query)
{ {
Benchmark::measure('DB is fetching All'); return $this->dbAdapter->fetchAll($query->getSelectQuery());
$result = $this->dbAdapter->fetchAll($query->getSelectQuery());
Benchmark::measure('DB fetch done');
return $result;
} }
/** /**
@ -215,21 +244,17 @@ class DbConnection implements Selectable
*/ */
public function fetchRow(DbQuery $query) public function fetchRow(DbQuery $query)
{ {
Benchmark::measure('DB is fetching row'); return $this->dbAdapter->fetchRow($query->getSelectQuery());
$result = $this->dbAdapter->fetchRow($query->getSelectQuery());
Benchmark::measure('DB row done');
return $result;
} }
/** /**
* 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 DbQuery $query
* @param int $columnIndex Index of the column to fetch
* *
* @return array * @return array
*/ */
public function fetchColumn(DbQuery $query, $columnIndex = 0) public function fetchColumn(DbQuery $query)
{ {
return $this->dbAdapter->fetchCol($query->getSelectQuery()); return $this->dbAdapter->fetchCol($query->getSelectQuery());
} }
@ -259,4 +284,158 @@ class DbConnection implements Selectable
{ {
return $this->dbAdapter->fetchPairs($query->getSelectQuery()); 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);
}
}
} }

View File

@ -4,13 +4,11 @@
namespace Icinga\Data\Db; namespace Icinga\Data\Db;
use Icinga\Data\SimpleQuery; use Icinga\Data\SimpleQuery;
use Icinga\Application\Benchmark;
use Icinga\Data\Filter\FilterChain; use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Data\Filter\FilterOr; use Icinga\Data\Filter\FilterOr;
use Icinga\Data\Filter\FilterAnd; use Icinga\Data\Filter\FilterAnd;
use Icinga\Data\Filter\FilterNot; use Icinga\Data\Filter\FilterNot;
use Icinga\Exception\IcingaException; use Icinga\Exception\QueryException;
use Zend_Db_Select; use Zend_Db_Select;
/** /**
@ -68,6 +66,7 @@ class DbQuery extends SimpleQuery
protected function init() protected function init()
{ {
$this->db = $this->ds->getDbAdapter(); $this->db = $this->ds->getDbAdapter();
$this->select = $this->db->select();
parent::init(); parent::init();
} }
@ -77,6 +76,13 @@ class DbQuery extends SimpleQuery
return $this; 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) public function where($condition, $value = null)
{ {
// $this->count = $this->select = null; // $this->count = $this->select = null;
@ -85,9 +91,6 @@ class DbQuery extends SimpleQuery
protected function dbSelect() protected function dbSelect()
{ {
if ($this->select === null) {
$this->select = $this->db->select()->from($this->target, array());
}
return clone $this->select; return clone $this->select;
} }
@ -153,7 +156,7 @@ class DbQuery extends SimpleQuery
$op = ' AND '; $op = ' AND ';
$str .= ' NOT '; $str .= ' NOT ';
} else { } else {
throw new IcingaException( throw new QueryException(
'Cannot render filter: %s', 'Cannot render filter: %s',
$filter $filter
); );
@ -214,7 +217,7 @@ class DbQuery extends SimpleQuery
if (! $value) { if (! $value) {
/* /*
NOTE: It's too late to throw exceptions, we might finish in __toString 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', '"%s" is not a valid time expression',
$value $value
)); ));
@ -298,10 +301,9 @@ class DbQuery extends SimpleQuery
public function count() public function count()
{ {
if ($this->count === null) { if ($this->count === null) {
Benchmark::measure('DB is counting'); $this->count = parent::count();
$this->count = $this->db->fetchOne($this->getCountQuery());
Benchmark::measure('DB finished count');
} }
return $this->count; return $this->count;
} }
@ -321,9 +323,7 @@ class DbQuery extends SimpleQuery
public function __clone() 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; $this->group = $group;
return $this; 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;
}
} }

View File

@ -0,0 +1,22 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data;
use Icinga\Exception\StatementException;
/**
* Interface for data insertion
*/
interface Extensible
{
/**
* Insert the given data for the given target
*
* @param string $target
* @param array $data
*
* @throws StatementException
*/
public function insert($target, array $data);
}

View File

@ -23,13 +23,11 @@ interface Fetchable
public function fetchRow(); public function fetchRow();
/** /**
* 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 int $columnIndex Index of the column to fetch
* *
* @return array * @return array
*/ */
public function fetchColumn($columnIndex = 0); public function fetchColumn();
/** /**
* Fetch the first column of the first row of the result set * Fetch the first column of the first row of the result set

View File

@ -0,0 +1,23 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\StatementException;
/**
* Interface for data deletion
*/
interface Reducible
{
/**
* Delete entries in the given target, optionally limiting the affected entries by using a filter
*
* @param string $target
* @param Filter $filter
*
* @throws StatementException
*/
public function delete($target, Filter $filter = null);
}

View File

@ -4,7 +4,6 @@
namespace Icinga\Data; namespace Icinga\Data;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Exception\ProgrammingError;
use Icinga\Util\ConfigAwareFactory; use Icinga\Util\ConfigAwareFactory;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Data\Db\DbConnection; use Icinga\Data\Db\DbConnection;
@ -70,13 +69,13 @@ class ResourceFactory implements ConfigAwareFactory
/** /**
* Check if the existing resources are set. If not, throw an error. * Check if the existing resources are set. If not, throw an error.
* *
* @throws ProgrammingError * @throws ConfigurationError
*/ */
private static function assertResourcesExist() private static function assertResourcesExist()
{ {
if (!isset(self::$resources)) { if (self::$resources === null) {
throw new ProgrammingError( throw new ConfigurationError(
'The ResourceFactory must be initialised by setting a config, before it can be used' 'Resources not set up. Please contact your Icinga Web administrator'
); );
} }
} }

View File

@ -3,15 +3,16 @@
namespace Icinga\Data; namespace Icinga\Data;
use ArrayIterator; use Iterator;
use IteratorAggregate; use IteratorAggregate;
use Zend_Paginator; use Zend_Paginator;
use Icinga\Application\Icinga; use Icinga\Application\Icinga;
use Icinga\Application\Benchmark;
use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\Filter;
use Icinga\Exception\IcingaException; use Icinga\Exception\IcingaException;
use Icinga\Web\Paginator\Adapter\QueryAdapter; use Icinga\Web\Paginator\Adapter\QueryAdapter;
class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate class SimpleQuery implements QueryInterface, Queryable, Iterator
{ {
/** /**
* Query data source * Query data source
@ -21,9 +22,18 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
protected $ds; protected $ds;
/** /**
* The table you are going to query * This query's iterator
*
* @var Iterator
*/ */
protected $table; protected $iterator;
/**
* The target you are going to query
*
* @var mixed
*/
protected $target;
/** /**
* The columns you asked for * The columns you asked for
@ -43,6 +53,15 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/ */
protected $columns = array(); protected $columns = array();
/**
* The columns and their aliases flipped in order to handle aliased sort columns
*
* Supposed to be used and populated by $this->compare *only*.
*
* @var array
*/
protected $flippedColumns;
/** /**
* The columns you're using to sort the query result * The columns you're using to sort the query result
* *
@ -92,16 +111,6 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/ */
protected function init() {} 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 * 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) public function from($target, array $fields = null)
{ {
@ -226,32 +299,42 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
return $this; 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($orderIndex, $this->order)) {
if (! array_key_exists($col_num, $this->order)) { return 0; // Last column to sort reached, rows are considered being equal
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 ($dir === self::SORT_ASC) { if ($this->flippedColumns === null) {
return $res; $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 { } else {
return $res * -1; return $result * -1;
} }
} }
@ -374,7 +457,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
*/ */
public function fetchAll() 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() 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 * Fetch the first column of all rows of the result set as an array
*
* @param int $columnIndex Index of the column to fetch
* *
* @return 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() 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() 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() 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) public function columns(array $columns)
{ {
$this->columns = $columns; $this->columns = $columns;
$this->flippedColumns = null; // Reset, due to updated columns
return $this; return $this;
} }

View File

@ -0,0 +1,24 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\StatementException;
/**
* Interface for data updating
*/
interface Updatable
{
/**
* 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
*/
public function update($target, array $data, Filter $filter = null);
}

View File

@ -0,0 +1,8 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Exception;
class StatementException extends IcingaException
{
}

View File

@ -4,6 +4,7 @@
namespace Icinga\Protocol\File; namespace Icinga\Protocol\File;
use Countable; use Countable;
use ArrayIterator;
use Icinga\Data\Selectable; use Icinga\Data\Selectable;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
@ -71,6 +72,18 @@ class FileReader implements Selectable, Countable
return new FileQuery($this); return new FileQuery($this);
} }
/**
* Fetch and return all rows of the given query's result set using an iterator
*
* @param FileQuery $query
*
* @return ArrayIterator
*/
public function query(FileQuery $query)
{
return new ArrayIterator($this->fetchAll($query));
}
/** /**
* Return the number of available valid lines. * Return the number of available valid lines.
* *

View File

@ -3,12 +3,14 @@
namespace Icinga\Protocol\Ldap; namespace Icinga\Protocol\Ldap;
use Icinga\Exception\ProgrammingError; use ArrayIterator;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Application\Platform;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Application\Logger; use Icinga\Application\Logger;
use Icinga\Application\Platform;
use Icinga\Data\ConfigObject; 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. * Backend class managing all the LDAP stuff for you.
@ -24,7 +26,7 @@ use Icinga\Data\ConfigObject;
* )); * ));
* </code> * </code>
*/ */
class Connection class Connection implements Selectable
{ {
const LDAP_NO_SUCH_OBJECT = 32; const LDAP_NO_SUCH_OBJECT = 32;
const LDAP_SIZELIMIT_EXCEEDED = 4; const LDAP_SIZELIMIT_EXCEEDED = 4;
@ -122,11 +124,21 @@ class Connection
return $this->root; return $this->root;
} }
/**
* Provide a query on this connection
*
* @return Query
*/
public function select() public function select()
{ {
return new Query($this); return new Query($this);
} }
public function query(Query $query)
{
return new ArrayIterator($this->fetchAll($query));
}
public function fetchOne($query, $fields = array()) public function fetchOne($query, $fields = array())
{ {
$row = (array) $this->fetchRow($query, $fields); $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 * @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 * @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); $rows = $this->fetchAll($query, $fields);
if (count($rows) !== 1) { if (count($rows) > 1) {
throw new LdapException( throw new LdapException(
'Cannot fetch single DN for %s', 'Cannot fetch single DN for %s',
$query->create() $query
); );
} }
return key($rows); return key($rows);
@ -235,14 +247,9 @@ class Connection
$this->connect(); $this->connect();
$this->bind(); $this->bind();
$count = 0; // TODO: That's still not the best solution, this should probably not request any attributes
$results = $this->runQuery($query); $res = $this->runQuery($query);
while (! empty($results)) { return count($res);
$count += ldap_count_entries($this->ds, $results);
$results = $this->runQuery($query);
}
return $count;
} }
public function fetchAll(Query $query, $fields = array()) public function fetchAll(Query $query, $fields = array())
@ -266,16 +273,20 @@ class Connection
* @return array The matched entries * @return array The matched entries
* @throws LdapException * @throws LdapException
*/ */
protected function runQuery(Query $query, $fields = array()) protected function runQuery(Query $query, array $fields = null)
{ {
$limit = $query->getLimit(); $limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
if (empty($fields)) {
$fields = $query->getColumns();
}
$results = @ldap_search( $results = @ldap_search(
$this->ds, $this->ds,
$query->hasBase() ? $query->getBase() : $this->root_dn, $query->getBase() ?: $this->root_dn,
$query->create(), (string) $query,
empty($fields) ? $query->listFields() : $fields, array_values($fields),
0, // Attributes and values 0, // Attributes and values
$limit ? $offset + $limit : 0 $limit ? $offset + $limit : 0
); );
@ -286,18 +297,14 @@ class Connection
throw new LdapException( throw new LdapException(
'LDAP query "%s" (base %s) failed. Error: %s', 'LDAP query "%s" (base %s) failed. Error: %s',
$query->create(), $query,
$query->hasBase() ? $query->getBase() : $this->root_dn, $query->getBase() ?: $this->root_dn,
ldap_error($this->ds) ldap_error($this->ds)
); );
} elseif (ldap_count_entries($this->ds, $results) === 0) { } elseif (ldap_count_entries($this->ds, $results) === 0) {
return array(); return array();
} }
foreach ($query->getSortColumns() as $col) {
ldap_sort($this->ds, $results, $col[0]);
}
$count = 0; $count = 0;
$entries = array(); $entries = array();
$entry = ldap_first_entry($this->ds, $results); $entry = ldap_first_entry($this->ds, $results);
@ -305,11 +312,15 @@ class Connection
$count += 1; $count += 1;
if ($offset === 0 || $offset < $count) { if ($offset === 0 || $offset < $count) {
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( $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))); } 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); ldap_free_result($results);
return $entries; return $entries;
} }
@ -330,28 +341,29 @@ class Connection
* *
* @param Query $query The query to execute * @param Query $query The query to execute
* @param array $fields The fields that will be fetched from the matches * @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 * @return array The matched entries
* @throws LdapException * @throws LdapException
* @throws ProgrammingError When executed without available page controls (check with pageControlAvailable() ) * @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)) { if (! $this->pageControlAvailable($query)) {
throw new ProgrammingError('LDAP: Page control not available.'); throw new ProgrammingError('LDAP: Page control not available.');
} }
if (! isset($pageSize)) { if (! isset($pageSize)) {
$pageSize = static::PAGE_SIZE; $pageSize = static::PAGE_SIZE;
} }
$limit = $query->getLimit(); $limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
$queryString = $query->create(); $queryString = (string) $query;
$base = $query->hasBase() ? $query->getBase() : $this->root_dn; $base = $query->getBase() ?: $this->root_dn;
if (empty($fields)) { if (empty($fields)) {
$fields = $query->listFields(); $fields = $query->getColumns();
} }
$count = 0; $count = 0;
@ -362,7 +374,14 @@ class Connection
// possibillity server to return an answer in case the pagination extension is missing. // possibillity server to return an answer in case the pagination extension is missing.
ldap_control_paged_result($this->ds, $pageSize, false, $cookie); 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 ($results === false) {
if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
break; break;
@ -394,7 +413,7 @@ class Connection
$count += 1; $count += 1;
if ($offset === 0 || $offset < $count) { if ($offset === 0 || $offset < $count) {
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( $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))); } 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 // 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 // the server: https://www.ietf.org/rfc/rfc2696.txt
ldap_control_paged_result($this->ds, 0, false, $cookie); 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 { } else {
// Reset the paged search request so that subsequent requests succeed // Reset the paged search request so that subsequent requests succeed
ldap_control_paged_result($this->ds, 0); ldap_control_paged_result($this->ds, 0);
} }
if ($query->hasOrder()) {
uasort($entries, array($query, 'compare'));
}
return $entries; return $entries;
} }
protected function cleanupAttributes($attrs) protected function cleanupAttributes($attributes, array $requestedFields)
{ {
$clean = (object) array(); // In case the result contains attributes with a differing case than the requested fields, it is
for ($i = 0; $i < $attrs['count']; $i++) { // necessary to create another array to map attributes case insensitively to their requested counterparts.
$attr_name = $attrs[$i]; // This does also apply the virtual alias handling. (Since an LDAP server does not handle such)
if ($attrs[$attr_name]['count'] === 1) { $loweredFieldMap = array();
$clean->$attr_name = $attrs[$attr_name][0]; 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 { } else {
for ($j = 0; $j < $attrs[$attr_name]['count']; $j++) { $attribute_value = array();
$clean->{$attr_name}[] = $attrs[$attr_name][$j]; 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) public function testCredentials($username, $password)
@ -566,6 +617,10 @@ class Connection
*/ */
public function getCapabilities() public function getCapabilities()
{ {
if ($this->capabilities === null) {
$this->connect(); // Populates $this->capabilities
}
return $this->capabilities; return $this->capabilities;
} }
@ -590,30 +645,22 @@ class Connection
*/ */
protected function discoverCapabilities($ds) protected function discoverCapabilities($ds)
{ {
$query = $this->select()->from( $fields = array(
'*', 'defaultNamingContext',
array( 'namingContexts',
'defaultNamingContext', 'vendorName',
'namingContexts', 'vendorVersion',
'vendorName', 'supportedSaslMechanisms',
'vendorVersion', 'dnsHostName',
'supportedSaslMechanisms', 'schemaNamingContext',
'dnsHostName', 'supportedLDAPVersion', // => array(3, 2)
'schemaNamingContext', 'supportedCapabilities',
'supportedLDAPVersion', // => array(3, 2) 'supportedControl',
'supportedCapabilities', 'supportedExtension',
'supportedControl', '+'
'supportedExtension',
'+'
)
);
$result = @ldap_read(
$ds,
'',
$query->create(),
$query->listFields()
); );
$result = @ldap_read($ds, '', (string) $this->select()->from('*', $fields), $fields);
if (! $result) { if (! $result) {
throw new LdapException( throw new LdapException(
'Capability query failed (%s:%d): %s. Check if hostname and port of the' 'Capability query failed (%s:%d): %s. Check if hostname and port of the'
@ -623,6 +670,7 @@ class Connection
ldap_error($ds) ldap_error($ds)
); );
} }
$entry = ldap_first_entry($ds, $result); $entry = ldap_first_entry($ds, $result);
if ($entry === false) { if ($entry === false) {
throw new LdapException( throw new LdapException(
@ -633,9 +681,7 @@ class Connection
); );
} }
$ldapAttributes = ldap_get_attributes($ds, $entry); return new Capability($this->cleanupAttributes(ldap_get_attributes($ds, $entry), array_flip($fields)));
$result = $this->cleanupAttributes($ldapAttributes);
return new Capability($result);
} }
/** /**

View File

@ -3,151 +3,154 @@
namespace Icinga\Protocol\Ldap; namespace Icinga\Protocol\Ldap;
use Icinga\Data\SimpleQuery;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotImplementedError;
/** /**
* Search class * LDAP query class
*
* @package Icinga\Protocol\Ldap
*/ */
/** class Query extends SimpleQuery
* Search abstraction class
*
* Usage example:
*
* <code>
* $connection->select()->from('user')->where('sAMAccountName = ?', 'icinga');
* </code>
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @package Icinga\Protocol\Ldap
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
*/
class Query
{ {
protected $connection; /**
protected $filters = array(); * This query's filters
protected $fields = array(); *
protected $limit_count = 0; * Currently just a basic key/value pair based array. Can be removed once Icinga\Data\Filter is supported.
protected $limit_offset = 0; *
protected $sort_columns = array(); * @var array
protected $count; */
protected $base; protected $filters;
protected $usePagedResults = true;
/** /**
* Constructor * The base dn being used for this query
* *
* @param Connection LDAP Connection object * @var string
* @return void
*/ */
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) public function setBase($base)
{ {
$this->base = $base; $this->base = $base;
return $this; return $this;
} }
public function hasBase() /**
{ * Return the base dn being used for this query
return $this->base !== null; *
} * @return string
*/
public function getBase() public function getBase()
{ {
return $this->base; return $this->base;
} }
/**
* Set whether this query is permitted to utilize paged results
*
* @param bool $state
*
* @return $this
*/
public function setUsePagedResults($state = true) public function setUsePagedResults($state = true)
{ {
$this->usePagedResults = (bool) $state; $this->usePagedResults = (bool) $state;
return $this; return $this;
} }
/**
* Return whether this query is permitted to utilize paged results
*
* @return bool
*/
public function getUsePagedResults() public function getUsePagedResults()
{ {
return $this->usePagedResults; 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->filters['objectClass'] = $target;
$this->count = $this->connection->count($this); return parent::from($target, $fields);
}
return $this->count;
} }
/** /**
* 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)) { // TODO: Adjust this once support for Icinga\Data\Filter is available
throw new Exception( if ($condition instanceof Expression) {
'Got invalid limit: %s, %s', $this->filters[] = $condition;
$count, } else {
$offset $this->filters[$condition] = $value;
);
} }
$this->limit_count = (int) $count;
$this->limit_offset = (int) $offset;
return $this; return $this;
} }
/** public function getFilter()
* Whether a limit has been set
*
* @return boolean
*/
public function hasLimit()
{ {
return $this->limit_count > 0; throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
} }
/** public function addFilter(Filter $filter)
* Whether an offset (limit) has been set
*
* @return boolean
*/
public function hasOffset()
{ {
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);
}
}
} }
/** public function setFilter(Filter $filter)
* Retrieve result limit
*
* @return int
*/
public function getLimit()
{ {
return $this->limit_count; throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
}
/**
* Retrieve result offset
*
* @return int
*/
public function getOffset()
{
return $this->limit_offset;
} }
/** /**
* Fetch result as tree * 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() 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); if (! isset($this->filters['objectClass'])) {
}
/**
* 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) {
throw new Exception('Object class is mandatory'); throw new Exception('Object class is mandatory');
} }
$parts = array();
foreach ($this->filters as $key => $value) { foreach ($this->filters as $key => $value) {
if ($value instanceof Expression) { if ($value instanceof Expression) {
$parts[] = (string) $value; $parts[] = (string) $value;
@ -316,6 +219,7 @@ class Query
); );
} }
} }
if (count($parts) > 1) { if (count($parts) > 1) {
return '(&(' . implode(')(', $parts) . '))'; return '(&(' . implode(')(', $parts) . '))';
} else { } else {
@ -323,17 +227,13 @@ class Query
} }
} }
/**
* Return the LDAP filter to be applied on this query
*
* @return string
*/
public function __toString() public function __toString()
{ {
return $this->create(); return $this->renderFilter();
}
/**
* Descructor
*/
public function __destruct()
{
// To be on the safe side:
unset($this->connection);
} }
} }

View File

@ -0,0 +1,755 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Icinga\Data\Db\DbConnection;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Reducible;
use Icinga\Data\Updatable;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\StatementException;
use Icinga\Util\String;
/**
* Abstract base class for concrete database repository implementations
*
* Additionally provided features:
* <ul>
* <li>Support for table aliases</li>
* <li>Automatic table prefix handling</li>
* <li>Insert, update and delete capabilities</li>
* <li>Differentiation between statement and query columns</li>
* <li>Capability to join additional tables depending on the columns being selected or used in a filter</li>
* </ul>
*/
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:
* <pre><code>
* array(
* 'table_name' => array(
* 'column1',
* 'alias1' => 'column2',
* 'alias2' => 'column3'
* )
* )
* <pre><code>
*
* @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<TableName>
* 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<TableName>-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;
}
}
}
}
}

View File

@ -0,0 +1,186 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Exception;
use Icinga\Application\Config;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Updatable;
use Icinga\Data\Reducible;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\StatementException;
/**
* Abstract base class for concrete INI repository implementations
*
* Additionally provided features:
* <ul>
* <li>Insert, update and delete capabilities</li>
* </ul>
*/
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;
}
}

View File

@ -0,0 +1,861 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use DateTime;
use Icinga\Application\Logger;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Selectable;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\QueryException;
use Icinga\Exception\StatementException;
use Icinga\Util\String;
/**
* Abstract base class for concrete repository implementations
*
* To utilize this class and its features, the following is required:
* <ul>
* <li>Concrete implementations need to initialize Repository::$queryColumns</li>
* <li>The datasource passed to a repository must implement the Selectable interface</li>
* <li>The datasource must yield an instance of Queryable when its select() method is called</li>
* </ul>
*/
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
* <pre><code>
* array(
* 'baseTable' => array(
* 'column1',
* 'alias1' => 'column2',
* 'alias2' => 'column3'
* )
* )
* <pre><code>
*
* @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
* <pre><code>
* 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
* )
* )
* <pre><code>
* 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;
}
}

View File

@ -0,0 +1,589 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Iterator;
use IteratorAggregate;
use Icinga\Application\Benchmark;
use Icinga\Application\Logger;
use Icinga\Data\QueryInterface;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\QueryException;
/**
* Query class supposed to mediate between a repository and its datasource's query
*/
class RepositoryQuery implements QueryInterface, Iterator
{
/**
* The repository being used
*
* @var Repository
*/
protected $repository;
/**
* The real query being used
*
* @var QueryInterface
*/
protected $query;
/**
* The current target to be queried
*
* @var mixed
*/
protected $target;
/**
* The real query's iterator
*
* @var Iterator
*/
protected $iterator;
/**
* Create a new repository query
*
* @param Repository $repository The repository to use
*/
public function __construct(Repository $repository)
{
$this->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();
}
}

View File

@ -426,7 +426,7 @@ class User
// matches // matches
$any = strpos($requiredPermission, '*'); $any = strpos($requiredPermission, '*');
foreach ($this->permissions as $grantedPermission) { foreach ($this->permissions as $grantedPermission) {
if ($any !== false && strpos($grantedPermission, '*') === false) { if ($any !== false) {
$wildcard = $any; $wildcard = $any;
} else { } else {
// If the permit contains a wildcard, grant the permission if it's related to the permit // If the permit contains a wildcard, grant the permission if it's related to the permit

View File

@ -0,0 +1,198 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Controller;
use \Zend_Controller_Action_Exception;
use Icinga\Application\Config;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\UserBackendInterface;
use Icinga\Authentication\UserGroup\UserGroupBackend;
use Icinga\Authentication\UserGroup\UserGroupBackendInterface;
use Icinga\Security\SecurityException;
use Icinga\Web\Controller;
/**
* Base class for authentication backend controllers
*/
class AuthBackendController extends Controller
{
/**
* Redirect to the first permitted list action
*/
final public function indexAction()
{
if ($this->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;
}
}

View File

@ -39,7 +39,7 @@ class ErrorLabeller extends Zend_Translate_Adapter
protected function createMessages($element) protected function createMessages($element)
{ {
$label = $element->getLabel(); $label = $element->getLabel() ?: $element->getName();
return array( return array(
Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label), Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),

View File

@ -233,40 +233,50 @@ class Menu implements RecursiveIterator
)); ));
$section = $this->add(t('System'), array( $section = $this->add(t('System'), array(
'icon' => 'wrench', 'icon' => 'services',
'priority' => 200, 'priority' => 700,
'renderer' => 'ProblemMenuItemRenderer' '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', 'url' => 'config',
'permission' => 'config/application/*', 'permission' => 'config/application/*',
'priority' => 300 'priority' => 810
));
$section->add(t('Authentication'), array(
'url' => 'user',
'permission' => 'config/authentication/*',
'priority' => 820
)); ));
$section->add(t('Modules'), array( $section->add(t('Modules'), array(
'url' => 'config/modules', 'url' => 'config/modules',
'permission' => '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( $section = $this->add($auth->getUser()->getUsername(), array(
'icon' => 'user', 'icon' => 'user',
'priority' => 600 'priority' => 900
)); ));
$section->add(t('Preferences'), array( $section->add(t('Preferences'), array(
'url' => 'preference', 'url' => 'preference',
'priority' => 601 'priority' => 910
)); ));
$section->add(t('Logout'), array( $section->add(t('Logout'), array(
'url' => 'authentication/logout', 'url' => 'authentication/logout',
'priority' => 700, 'priority' => 990,
'renderer' => 'ForeignMenuItemRenderer' 'renderer' => 'ForeignMenuItemRenderer'
)); ));
} }

View File

@ -4,44 +4,64 @@
namespace Icinga\Web\Paginator\Adapter; namespace Icinga\Web\Paginator\Adapter;
use Zend_Paginator_Adapter_Interface; use Zend_Paginator_Adapter_Interface;
use Icinga\Data\QueryInterface;
/**
* @see Zend_Paginator_Adapter_Interface
*/
class QueryAdapter implements Zend_Paginator_Adapter_Interface class QueryAdapter implements Zend_Paginator_Adapter_Interface
{ {
/** /**
* Array * The query being paginated
* *
* @var array * @var QueryInterface
*/ */
protected $query = null; protected $query;
/** /**
* Item count * 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(QueryInterface $query)
public function __construct($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 QueryInterface $query
* @param integer $itemCountPerPage Number of items per page *
* @return array * @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) 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() public function count()
{ {
if ($this->count === null) { if ($this->count === null) {
$this->count = $this->query->count(); $this->count = $this->query->count();
} }
return $this->count; return $this->count;
} }
} }

View File

@ -7,7 +7,7 @@ $section = $this->menuSection($this->translate('Documentation'), array(
'title' => 'Documentation', 'title' => 'Documentation',
'icon' => 'book', 'icon' => 'book',
'url' => 'doc', 'url' => 'doc',
'priority' => 190 'priority' => 700
)); ));
$section->add('Icinga Web 2', array( $section->add('Icinga Web 2', array(
@ -18,7 +18,7 @@ $section->add('Module documentations', array(
)); ));
$section->add($this->translate('Developer - Style'), array( $section->add($this->translate('Developer - Style'), array(
'url' => 'doc/style/guide', 'url' => 'doc/style/guide',
'priority' => 200, 'priority' => 790
)); ));
$this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10); $this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10);

View File

@ -208,7 +208,7 @@ $section->add($this->translate('Alert Summary'), array(
$section = $this->menuSection($this->translate('System')); $section = $this->menuSection($this->translate('System'));
$section->add($this->translate('Monitoring Health'), array( $section->add($this->translate('Monitoring Health'), array(
'url' => 'monitoring/process/info', 'url' => 'monitoring/process/info',
'priority' => 120, 'priority' => 720,
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer' 'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer'
)); ));

View File

@ -415,21 +415,11 @@ abstract class IdoQuery extends DbQuery
} elseif ($dbType === 'pgsql') { } elseif ($dbType === 'pgsql') {
$this->initializeForPostgres(); $this->initializeForPostgres();
} }
$this->dbSelect(); $this->joinBaseTables();
$this->select->columns($this->columns); $this->select->columns($this->columns);
//$this->joinBaseTables();
$this->prepareAliasIndexes(); $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 * Join the base tables for this query
*/ */

View File

@ -108,7 +108,8 @@ abstract class CommandTransport
*/ */
public static function first() public static function first()
{ {
$config = self::getConfig()->current(); $config = self::getConfig();
return self::fromConfig($config); $config->rewind();
return self::fromConfig($config->current());
} }
} }

View File

@ -3,7 +3,6 @@
namespace Icinga\Module\Monitoring\DataView; namespace Icinga\Module\Monitoring\DataView;
use ArrayIterator;
use IteratorAggregate; use IteratorAggregate;
use Icinga\Data\QueryInterface; use Icinga\Data\QueryInterface;
use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\Filter;
@ -13,6 +12,7 @@ use Icinga\Data\ConnectionInterface;
use Icinga\Exception\QueryException; use Icinga\Exception\QueryException;
use Icinga\Web\Request; use Icinga\Web\Request;
use Icinga\Web\Url; use Icinga\Web\Url;
use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
use Icinga\Module\Monitoring\Backend\MonitoringBackend; use Icinga\Module\Monitoring\Backend\MonitoringBackend;
/** /**
@ -23,7 +23,7 @@ abstract class DataView implements QueryInterface, IteratorAggregate
/** /**
* The query used to populate the view * The query used to populate the view
* *
* @var QueryInterface * @var IdoQuery
*/ */
protected $query; protected $query;
@ -61,11 +61,11 @@ abstract class DataView implements QueryInterface, IteratorAggregate
/** /**
* Return a iterator for all rows of the result set * Return a iterator for all rows of the result set
* *
* @return ArrayIterator * @return IdoQuery
*/ */
public function getIterator() 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 * Fetch the first column of all rows of the result set as an array
*
* @param int $columnIndex Index of the column to fetch
* *
* @return array * @return array
*/ */
public function fetchColumn($columnIndex = 0) public function fetchColumn()
{ {
return $this->getQuery()->fetchColumn($columnIndex); return $this->getQuery()->fetchColumn();
} }
/** /**

View File

@ -8,8 +8,8 @@ use LogicException;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Authentication\Backend\DbUserBackend; use Icinga\Authentication\User\DbUserBackend;
use Icinga\Authentication\Backend\LdapUserBackend; use Icinga\Authentication\User\LdapUserBackend;
/** /**
* Wizard page to define the initial administrative account * Wizard page to define the initial administrative account
@ -268,13 +268,8 @@ class AdminAccountPage extends Form
if ($this->backendConfig['backend'] === 'db') { if ($this->backendConfig['backend'] === 'db') {
$backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig))); $backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
} elseif ($this->backendConfig['backend'] === 'ldap') { } elseif ($this->backendConfig['backend'] === 'ldap') {
$backend = new LdapUserBackend( $backend = new LdapUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
ResourceFactory::createResource(new ConfigObject($this->resourceConfig)), $backend->setConfig($this->backendConfig);
$this->backendConfig['user_class'],
$this->backendConfig['user_name_attribute'],
$this->backendConfig['base_dn'],
$this->backendConfig['filter']
);
} else { } else {
throw new LogicException( throw new LogicException(
sprintf( sprintf(
@ -285,10 +280,8 @@ class AdminAccountPage extends Form
} }
try { try {
$users = $backend->listUsers(); return $backend->select(array('user_name'))->fetchColumn();
natsort ($users); } catch (Exception $_) {
return $users;
} catch (Exception $e) {
// No need to handle anything special here. Error means no users found. // No need to handle anything special here. Error means no users found.
return array(); return array();
} }

View File

@ -4,9 +4,9 @@
namespace Icinga\Module\Setup\Forms; namespace Icinga\Module\Setup\Forms;
use Icinga\Web\Form; use Icinga\Web\Form;
use Icinga\Forms\Config\Authentication\DbBackendForm; use Icinga\Forms\Config\UserBackend\DbBackendForm;
use Icinga\Forms\Config\Authentication\LdapBackendForm; use Icinga\Forms\Config\UserBackend\LdapBackendForm;
use Icinga\Forms\Config\Authentication\ExternalBackendForm; use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
/** /**
@ -105,7 +105,7 @@ class AuthBackendPage extends Form
} }
if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) { 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(); $this->addSkipValidationCheckbox();
return false; return false;
} }

View File

@ -7,7 +7,7 @@ use Exception;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Authentication\Backend\DbUserBackend; use Icinga\Authentication\User\DbUserBackend;
use Icinga\Module\Setup\Step; use Icinga\Module\Setup\Step;
class AuthenticationStep extends Step class AuthenticationStep extends Step
@ -88,11 +88,12 @@ class AuthenticationStep extends Step
ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig'])) ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig']))
); );
if (array_search($this->data['adminAccountData']['username'], $backend->listUsers()) === false) { if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) {
$backend->addUser( $backend->insert('user', array(
$this->data['adminAccountData']['username'], 'user_name' => $this->data['adminAccountData']['username'],
$this->data['adminAccountData']['password'] 'password' => $this->data['adminAccountData']['password'],
); 'is_active' => true
));
} }
} catch (Exception $e) { } catch (Exception $e) {
$this->dbError = $e; $this->dbError = $e;

View File

@ -41,6 +41,10 @@ img.icon {
background-position: 1em center; background-position: 1em center;
} }
#notifications > li.info {
background-color: @colorFormNotificationInfo;
}
#notifications > li.warning { #notifications > li.warning {
background-color: @colorWarningHandled; background-color: @colorWarningHandled;
} }
@ -202,3 +206,164 @@ table.benchmark {
border: 1px solid lightgrey; border: 1px solid lightgrey;
background-color: #fbfcc5; 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;
}
}

View File

@ -310,7 +310,7 @@
} else { } else {
if (req.$target.attr('id') === 'col2') { // TODO: multicol 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(); icinga.ui.layout1col();
req.$target = $('#col1'); req.$target = $('#col1');
delete(this.requests['col2']); delete(this.requests['col2']);

View File

@ -1,7 +1,7 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\Forms\Config\Authentication; namespace Tests\Icinga\Forms\Config\UserBackend;
// Necessary as some of these tests disable phpunit's preservation // Necessary as some of these tests disable phpunit's preservation
// of the global state (e.g. autoloaders are in the global state) // of the global state (e.g. autoloaders are in the global state)
@ -10,7 +10,7 @@ require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php');
use Mockery; use Mockery;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
use Icinga\Forms\Config\Authentication\DbBackendForm; use Icinga\Forms\Config\UserBackend\DbBackendForm;
class DbBackendFormTest extends BaseTestCase class DbBackendFormTest extends BaseTestCase
{ {
@ -27,12 +27,12 @@ class DbBackendFormTest extends BaseTestCase
public function testValidBackendIsValid() public function testValidBackendIsValid()
{ {
$this->setUpResourceFactoryMock(); $this->setUpResourceFactoryMock();
Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend') Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
->shouldReceive('count') ->shouldReceive('select->where->count')
->andReturn(2); ->andReturn(2);
// Passing array(null) is required to make Mockery call the constructor... // 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') $form->shouldReceive('getView->escape')
->with(Mockery::type('string')) ->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; }); ->andReturnUsing(function ($s) { return $s; });
@ -41,8 +41,8 @@ class DbBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_db_backend')); $form->populate(array('resource' => 'test_db_backend'));
$this->assertTrue( $this->assertTrue(
DbBackendForm::isValidAuthenticationBackend($form), DbBackendForm::isValidUserBackend($form),
'DbBackendForm claims that a valid authentication backend with users is not valid' 'DbBackendForm claims that a valid user backend with users is not valid'
); );
} }
@ -53,12 +53,12 @@ class DbBackendFormTest extends BaseTestCase
public function testInvalidBackendIsNotValid() public function testInvalidBackendIsNotValid()
{ {
$this->setUpResourceFactoryMock(); $this->setUpResourceFactoryMock();
Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend') Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
->shouldReceive('count') ->shouldReceive('count')
->andReturn(0); ->andReturn(0);
// Passing array(null) is required to make Mockery call the constructor... // 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') $form->shouldReceive('getView->escape')
->with(Mockery::type('string')) ->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; }); ->andReturnUsing(function ($s) { return $s; });
@ -67,8 +67,8 @@ class DbBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_db_backend')); $form->populate(array('resource' => 'test_db_backend'));
$this->assertFalse( $this->assertFalse(
DbBackendForm::isValidAuthenticationBackend($form), DbBackendForm::isValidUserBackend($form),
'DbBackendForm claims that an invalid authentication backend without users is valid' 'DbBackendForm claims that an invalid user backend without users is valid'
); );
} }

View File

@ -1,7 +1,7 @@
<?php <?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\Forms\Config\Authentication; namespace Tests\Icinga\Forms\Config\UserBackend;
// Necessary as some of these tests disable phpunit's preservation // Necessary as some of these tests disable phpunit's preservation
// of the global state (e.g. autoloaders are in the global state) // of the global state (e.g. autoloaders are in the global state)
@ -10,7 +10,7 @@ require_once realpath(dirname(__FILE__) . '/../../../../bootstrap.php');
use Mockery; use Mockery;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
use Icinga\Forms\Config\Authentication\LdapBackendForm; use Icinga\Forms\Config\UserBackend\LdapBackendForm;
use Icinga\Exception\AuthenticationException; use Icinga\Exception\AuthenticationException;
class LdapBackendFormTest extends BaseTestCase class LdapBackendFormTest extends BaseTestCase
@ -28,11 +28,12 @@ class LdapBackendFormTest extends BaseTestCase
public function testValidBackendIsValid() public function testValidBackendIsValid()
{ {
$this->setUpResourceFactoryMock(); $this->setUpResourceFactoryMock();
Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend') Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
->shouldReceive('assertAuthenticationPossible')->andReturnNull(); ->shouldReceive('assertAuthenticationPossible')->andReturnNull()
->shouldReceive('setConfig')->andReturnNull();
// Passing array(null) is required to make Mockery call the constructor... // 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') $form->shouldReceive('getView->escape')
->with(Mockery::type('string')) ->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; }); ->andReturnUsing(function ($s) { return $s; });
@ -41,8 +42,8 @@ class LdapBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_ldap_backend')); $form->populate(array('resource' => 'test_ldap_backend'));
$this->assertTrue( $this->assertTrue(
LdapBackendForm::isValidAuthenticationBackend($form), LdapBackendForm::isValidUserBackend($form),
'LdapBackendForm claims that a valid authentication backend with users is not valid' 'LdapBackendForm claims that a valid user backend with users is not valid'
); );
} }
@ -53,11 +54,11 @@ class LdapBackendFormTest extends BaseTestCase
public function testInvalidBackendIsNotValid() public function testInvalidBackendIsNotValid()
{ {
$this->setUpResourceFactoryMock(); $this->setUpResourceFactoryMock();
Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend') Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException); ->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException);
// Passing array(null) is required to make Mockery call the constructor... // 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') $form->shouldReceive('getView->escape')
->with(Mockery::type('string')) ->with(Mockery::type('string'))
->andReturnUsing(function ($s) { return $s; }); ->andReturnUsing(function ($s) { return $s; });
@ -66,8 +67,8 @@ class LdapBackendFormTest extends BaseTestCase
$form->populate(array('resource' => 'test_ldap_backend')); $form->populate(array('resource' => 'test_ldap_backend'));
$this->assertFalse( $this->assertFalse(
LdapBackendForm::isValidAuthenticationBackend($form), LdapBackendForm::isValidUserBackend($form),
'LdapBackendForm claims that an invalid authentication backend without users is valid' 'LdapBackendForm claims that an invalid user backend without users is valid'
); );
} }

View File

@ -5,10 +5,10 @@ namespace Tests\Icinga\Forms\Config;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
use Icinga\Application\Config; use Icinga\Application\Config;
use Icinga\Forms\Config\AuthenticationBackendConfigForm; use Icinga\Forms\Config\UserBackendConfigForm;
use Icinga\Forms\Config\AuthenticationBackendReorderForm; use Icinga\Forms\Config\UserBackendReorderForm;
class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendConfigForm class UserBackendConfigFormWithoutSave extends UserBackendConfigForm
{ {
public static $newConfig; public static $newConfig;
@ -19,11 +19,11 @@ class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendCo
} }
} }
class AuthenticationBackendReorderFormProvidingConfigFormWithoutSave extends AuthenticationBackendReorderForm class UserBackendReorderFormProvidingConfigFormWithoutSave extends UserBackendReorderForm
{ {
public function getConfigForm() public function getConfigForm()
{ {
$form = new AuthenticationBackendConfigFormWithoutSave(); $form = new UserBackendConfigFormWithoutSave();
$form->setIniConfig($this->config); $form->setIniConfig($this->config);
return $form; return $form;
} }
@ -45,7 +45,7 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
->shouldReceive('isPost')->andReturn(true) ->shouldReceive('isPost')->andReturn(true)
->shouldReceive('getPost')->andReturn(array('backend_newpos' => 'test3|1')); ->shouldReceive('getPost')->andReturn(array('backend_newpos' => 'test3|1'));
$form = new AuthenticationBackendReorderFormProvidingConfigFormWithoutSave(); $form = new UserBackendReorderFormProvidingConfigFormWithoutSave();
$form->setIniConfig($config); $form->setIniConfig($config);
$form->setTokenDisabled(); $form->setTokenDisabled();
$form->setUidDisabled(); $form->setUidDisabled();
@ -53,8 +53,8 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
$this->assertEquals( $this->assertEquals(
array('test1', 'test3', 'test2'), array('test1', 'test3', 'test2'),
AuthenticationBackendConfigFormWithoutSave::$newConfig->keys(), UserBackendConfigFormWithoutSave::$newConfig->keys(),
'Moving elements with AuthenticationBackendReorderForm does not seem to properly work' 'Moving elements with UserBackendReorderForm does not seem to properly work'
); );
} }
} }

View File

@ -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() public function testWhetherConfigObjectsAreTraversable()
{ {
$config = new ConfigObject(array('a' => 'b', 'c' => 'd')); $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() public function testWhetherItIsNotPossibleToAppendProperties()
{ {
@ -142,9 +134,6 @@ class ConfigObjectTest extends BaseTestCase
$this->assertFalse(isset($config->c), 'ConfigObjects do not allow to unset sections'); $this->assertFalse(isset($config->c), 'ConfigObjects do not allow to unset sections');
} }
/**
* @depends testWhetherConfigObjectsAreCountable
*/
public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections() public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections()
{ {
$config = new ConfigObject(); $config = new ConfigObject();

View File

@ -1,258 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
// We need to overwrite the library functions in the regular namespace to mock library functions. We run
// this test in a separate process to not alter different test cases.
namespace Icinga\Protocol\Ldap;
use Icinga\Test\BaseTestCase;
use Icinga\Data\ConfigObject;
use Mockery;
/**
* @runTestsInSeparateProcesses
*/
class ConnectionTest extends BaseTestCase
{
private $connection;
public $pagedResultsCalled;
public $startTlsCalled;
public $activatedOptions;
private function mockLdapFunctions () {
global $self;
$self = $this;
function ldap_connect()
{
return true;
}
function ldap_bind()
{
return true;
}
function ldap_search ()
{
return true;
}
function ldap_get_entries()
{
return 1;
}
function ldap_count_entries()
{
return 1;
}
function ldap_read()
{
return true;
}
function ldap_first_entry($ds, $result)
{
return $result;
}
function ldap_get_attributes()
{
global $self;
return $self->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.');
}
}

View File

@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase
return $select; 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() public function testFetchTree()
{ {
$this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP'); $this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP');
} }
public function testFrom()
{
return $this->testListFields();
}
public function testWhere() public function testWhere()
{ {
$this->markTestIncomplete('testWhere is not implemented yet'); $this->markTestIncomplete('testWhere is not implemented yet');
@ -88,30 +48,13 @@ class QueryTest extends BaseTestCase
public function testOrder() public function testOrder()
{ {
$select = $this->emptySelect()->order('bla'); $this->markTestIncomplete('testOrder is not implemented yet, order support for ldap queries is incomplete');
// tested by testGetSortColumns
} }
public function testListFields() public function testRenderFilter()
{
$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()
{ {
$select = $this->prepareSelect(); $select = $this->prepareSelect();
$res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))'; $res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))';
$this->assertEquals($res, $select->create()); $this->assertEquals($res, (string) $select);
} }
} }

View File

@ -67,13 +67,15 @@ class UserTest extends BaseTestCase
'test', 'test',
'test/some/specific', 'test/some/specific',
'test/more/*', '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'));
$this->assertTrue($user->can('test/some/specific')); $this->assertTrue($user->can('test/some/specific'));
$this->assertTrue($user->can('test/more/everything')); $this->assertTrue($user->can('test/more/everything'));
$this->assertTrue($user->can('test/wildcard-with-wildcard/*')); $this->assertTrue($user->can('test/wildcard-with-wildcard/*'));
$this->assertTrue($user->can('test/wildcard-with-wildcard/sub/sub')); $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('not/test'));
$this->assertFalse($user->can('test/some/not/so/specific')); $this->assertFalse($user->can('test/some/not/so/specific'));
$this->assertFalse($user->can('test/wildcard2/*')); $this->assertFalse($user->can('test/wildcard2/*'));