Merge branch 'feature/user-and-group-management-8826'
resolves #8826 resolves #9122 resolves #8877
This commit is contained in:
commit
5f898a3e23
|
@ -7,7 +7,7 @@ use Icinga\Application\Config;
|
|||
use Icinga\Application\Icinga;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Authentication\AuthChain;
|
||||
use Icinga\Authentication\Backend\ExternalBackend;
|
||||
use Icinga\Authentication\User\ExternalBackend;
|
||||
use Icinga\Exception\AuthenticationException;
|
||||
use Icinga\Exception\ConfigurationError;
|
||||
use Icinga\Exception\NotReadableError;
|
||||
|
|
|
@ -5,14 +5,15 @@ use Icinga\Application\Config;
|
|||
use Icinga\Application\Icinga;
|
||||
use Icinga\Application\Modules\Module;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
use Icinga\Forms\Config\AuthenticationBackendConfigForm;
|
||||
use Icinga\Forms\Config\AuthenticationBackendReorderForm;
|
||||
use Icinga\Forms\Config\UserBackendConfigForm;
|
||||
use Icinga\Forms\Config\UserBackendReorderForm;
|
||||
use Icinga\Forms\Config\GeneralConfigForm;
|
||||
use Icinga\Forms\Config\ResourceConfigForm;
|
||||
use Icinga\Forms\ConfirmRemovalForm;
|
||||
use Icinga\Security\SecurityException;
|
||||
use Icinga\Web\Controller;
|
||||
use Icinga\Web\Notification;
|
||||
use Icinga\Web\Url;
|
||||
use Icinga\Web\Widget;
|
||||
|
||||
/**
|
||||
|
@ -38,20 +39,12 @@ class ConfigController extends Controller
|
|||
$auth = $this->Auth();
|
||||
$allowedActions = array();
|
||||
if ($auth->hasPermission('config/application/general')) {
|
||||
$tabs->add('application', array(
|
||||
$tabs->add('general', array(
|
||||
'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
|
||||
'label' => $this->translate('Application'),
|
||||
'url' => 'config/application'
|
||||
'label' => $this->translate('General'),
|
||||
'url' => 'config/general'
|
||||
));
|
||||
$allowedActions[] = 'application';
|
||||
}
|
||||
if ($auth->hasPermission('config/application/authentication')) {
|
||||
$tabs->add('authentication', array(
|
||||
'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
|
||||
'label' => $this->translate('Authentication'),
|
||||
'url' => 'config/authentication'
|
||||
));
|
||||
$allowedActions[] = 'authentication';
|
||||
$allowedActions[] = 'general';
|
||||
}
|
||||
if ($auth->hasPermission('config/application/resources')) {
|
||||
$tabs->add('resource', array(
|
||||
|
@ -61,15 +54,21 @@ class ConfigController extends Controller
|
|||
));
|
||||
$allowedActions[] = 'resource';
|
||||
}
|
||||
if ($auth->hasPermission('config/application/roles')) {
|
||||
$tabs->add('roles', array(
|
||||
'title' => $this->translate(
|
||||
'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
|
||||
),
|
||||
'label' => $this->translate('Roles'),
|
||||
'url' => 'roles'
|
||||
if ($auth->hasPermission('config/application/userbackend')) {
|
||||
$tabs->add('userbackend', array(
|
||||
'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
|
||||
'label' => $this->translate('Authentication'),
|
||||
'url' => 'config/userbackend'
|
||||
));
|
||||
$allowedActions[] = 'roles';
|
||||
$allowedActions[] = 'userbackend';
|
||||
}
|
||||
if ($auth->hasPermission('config/application/usergroupbackend')) {
|
||||
$tabs->add('usergroupbackend', array(
|
||||
'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'),
|
||||
'label' => $this->translate('User Groups'),
|
||||
'url' => 'usergroupbackend/list'
|
||||
));
|
||||
$allowedActions[] = 'usergroupbackend';
|
||||
}
|
||||
$this->firstAllowedAction = array_shift($allowedActions);
|
||||
}
|
||||
|
@ -85,7 +84,7 @@ class ConfigController extends Controller
|
|||
public function indexAction()
|
||||
{
|
||||
if ($this->firstAllowedAction === null) {
|
||||
throw new SecurityException($this->translate('No permission for configuration'));
|
||||
throw new SecurityException($this->translate('No permission for application configuration'));
|
||||
}
|
||||
$action = $this->getTabs()->get($this->firstAllowedAction);
|
||||
if (substr($action->getUrl()->getPath(), 0, 7) === 'config/') {
|
||||
|
@ -96,11 +95,11 @@ class ConfigController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Application configuration
|
||||
* General configuration
|
||||
*
|
||||
* @throws SecurityException If the user lacks the permission for configuring the application
|
||||
* @throws SecurityException If the user lacks the permission for configuring the general configuration
|
||||
*/
|
||||
public function applicationAction()
|
||||
public function generalAction()
|
||||
{
|
||||
$this->assertPermission('config/application/general');
|
||||
$form = new GeneralConfigForm();
|
||||
|
@ -108,7 +107,7 @@ class ConfigController extends Controller
|
|||
$form->handleRequest();
|
||||
|
||||
$this->view->form = $form;
|
||||
$this->view->tabs->activate('application');
|
||||
$this->view->tabs->activate('general');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -201,71 +200,72 @@ class ConfigController extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Action for listing and reordering authentication backends
|
||||
* Action for listing and reordering user backends
|
||||
*/
|
||||
public function authenticationAction()
|
||||
public function userbackendAction()
|
||||
{
|
||||
$this->assertPermission('config/application/authentication');
|
||||
$form = new AuthenticationBackendReorderForm();
|
||||
$this->assertPermission('config/application/userbackend');
|
||||
$form = new UserBackendReorderForm();
|
||||
$form->setIniConfig(Config::app('authentication'));
|
||||
$form->handleRequest();
|
||||
|
||||
$this->view->form = $form;
|
||||
$this->view->tabs->activate('authentication');
|
||||
$this->render('authentication/reorder');
|
||||
$this->view->tabs->activate('userbackend');
|
||||
$this->render('userbackend/reorder');
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for creating a new authentication backend
|
||||
* Action for creating a new user backend
|
||||
*/
|
||||
public function createauthenticationbackendAction()
|
||||
public function createuserbackendAction()
|
||||
{
|
||||
$this->assertPermission('config/application/authentication');
|
||||
$form = new AuthenticationBackendConfigForm();
|
||||
$form->setTitle($this->translate('Create New Authentication Backend'));
|
||||
$this->assertPermission('config/application/userbackend');
|
||||
$form = new UserBackendConfigForm();
|
||||
$form->setTitle($this->translate('Create New User Backend'));
|
||||
$form->addDescription($this->translate(
|
||||
'Create a new backend for authenticating your users. This backend'
|
||||
. ' will be added at the end of your authentication order.'
|
||||
));
|
||||
$form->setIniConfig(Config::app('authentication'));
|
||||
$form->setResourceConfig(ResourceFactory::getResourceConfigs());
|
||||
$form->setRedirectUrl('config/authentication');
|
||||
$form->setRedirectUrl('config/userbackend');
|
||||
$form->handleRequest();
|
||||
|
||||
$this->view->form = $form;
|
||||
$this->view->tabs->activate('authentication');
|
||||
$this->render('authentication/create');
|
||||
$this->view->tabs->activate('userbackend');
|
||||
$this->render('userbackend/create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for editing authentication backends
|
||||
* Action for editing user backends
|
||||
*/
|
||||
public function editauthenticationbackendAction()
|
||||
public function edituserbackendAction()
|
||||
{
|
||||
$this->assertPermission('config/application/authentication');
|
||||
$form = new AuthenticationBackendConfigForm();
|
||||
$form->setTitle($this->translate('Edit Backend'));
|
||||
$this->assertPermission('config/application/userbackend');
|
||||
$form = new UserBackendConfigForm();
|
||||
$form->setTitle($this->translate('Edit User Backend'));
|
||||
$form->setIniConfig(Config::app('authentication'));
|
||||
$form->setResourceConfig(ResourceFactory::getResourceConfigs());
|
||||
$form->setRedirectUrl('config/authentication');
|
||||
$form->setRedirectUrl('config/userbackend');
|
||||
$form->setAction(Url::fromRequest());
|
||||
$form->handleRequest();
|
||||
|
||||
$this->view->form = $form;
|
||||
$this->view->tabs->activate('authentication');
|
||||
$this->render('authentication/modify');
|
||||
$this->view->tabs->activate('userbackend');
|
||||
$this->render('userbackend/modify');
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for removing a backend from the authentication list
|
||||
* Action for removing a user backend
|
||||
*/
|
||||
public function removeauthenticationbackendAction()
|
||||
public function removeuserbackendAction()
|
||||
{
|
||||
$this->assertPermission('config/application/authentication');
|
||||
$this->assertPermission('config/application/userbackend');
|
||||
$form = new ConfirmRemovalForm(array(
|
||||
'onSuccess' => function ($form) {
|
||||
$configForm = new AuthenticationBackendConfigForm();
|
||||
$configForm = new UserBackendConfigForm();
|
||||
$configForm->setIniConfig(Config::app('authentication'));
|
||||
$authBackend = $form->getRequest()->getQuery('auth_backend');
|
||||
$authBackend = $form->getRequest()->getQuery('backend');
|
||||
|
||||
try {
|
||||
$configForm->remove($authBackend);
|
||||
|
@ -276,7 +276,7 @@ class ConfigController extends Controller
|
|||
|
||||
if ($configForm->save()) {
|
||||
Notification::success(sprintf(
|
||||
t('Authentication backend "%s" has been successfully removed'),
|
||||
t('User backend "%s" has been successfully removed'),
|
||||
$authBackend
|
||||
));
|
||||
} else {
|
||||
|
@ -284,13 +284,14 @@ class ConfigController extends Controller
|
|||
}
|
||||
}
|
||||
));
|
||||
$form->setTitle($this->translate('Remove Backend'));
|
||||
$form->setRedirectUrl('config/authentication');
|
||||
$form->setTitle($this->translate('Remove User Backend'));
|
||||
$form->setRedirectUrl('config/userbackend');
|
||||
$form->setAction(Url::fromRequest());
|
||||
$form->handleRequest();
|
||||
|
||||
$this->view->form = $form;
|
||||
$this->view->tabs->activate('authentication');
|
||||
$this->render('authentication/remove');
|
||||
$this->view->tabs->activate('userbackend');
|
||||
$this->render('userbackend/remove');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -373,7 +374,7 @@ class ConfigController extends Controller
|
|||
if ($config->get('resource') === $resource) {
|
||||
$form->addDescription(sprintf(
|
||||
$this->translate(
|
||||
'The resource "%s" is currently in use by the authentication backend "%s". ' .
|
||||
'The resource "%s" is currently utilized for authentication by user backend "%s". ' .
|
||||
'Removing the resource can result in noone being able to log in any longer.'
|
||||
),
|
||||
$resource,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -4,69 +4,27 @@
|
|||
use Icinga\Application\Config;
|
||||
use Icinga\Forms\ConfirmRemovalForm;
|
||||
use Icinga\Forms\Security\RoleForm;
|
||||
use Icinga\Web\Controller\ActionController;
|
||||
use Icinga\Web\Controller\AuthBackendController;
|
||||
use Icinga\Web\Notification;
|
||||
use Icinga\Web\Widget;
|
||||
|
||||
/**
|
||||
* Roles configuration
|
||||
*/
|
||||
class RolesController extends ActionController
|
||||
class RoleController extends AuthBackendController
|
||||
{
|
||||
/**
|
||||
* Initialize tabs and validate the user's permissions
|
||||
*
|
||||
* @throws \Icinga\Security\SecurityException If the user lacks permissions for configuring roles
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->assertPermission('config/application/roles');
|
||||
$tabs = $this->getTabs();
|
||||
$auth = $this->Auth();
|
||||
if ($auth->hasPermission('config/application/general')) {
|
||||
$tabs->add('application', array(
|
||||
'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
|
||||
'label' => $this->translate('Application'),
|
||||
'url' => 'config'
|
||||
));
|
||||
}
|
||||
if ($auth->hasPermission('config/application/authentication')) {
|
||||
$tabs->add('authentication', array(
|
||||
'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
|
||||
'label' => $this->translate('Authentication'),
|
||||
'url' => 'config/authentication'
|
||||
));
|
||||
}
|
||||
if ($auth->hasPermission('config/application/resources')) {
|
||||
$tabs->add('resource', array(
|
||||
'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'),
|
||||
'label' => $this->translate('Resources'),
|
||||
'url' => 'config/resource'
|
||||
));
|
||||
}
|
||||
$tabs->add('roles', array(
|
||||
'title' => $this->translate(
|
||||
'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
|
||||
),
|
||||
'label' => $this->translate('Roles'),
|
||||
'url' => 'roles'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* List roles
|
||||
*/
|
||||
public function indexAction()
|
||||
public function listAction()
|
||||
{
|
||||
$this->view->tabs->activate('roles');
|
||||
$this->assertPermission('config/authentication/roles/show');
|
||||
$this->createListTabs()->activate('role/list');
|
||||
$this->view->roles = Config::app('roles', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new role
|
||||
*/
|
||||
public function newAction()
|
||||
public function addAction()
|
||||
{
|
||||
$this->assertPermission('config/authentication/roles/add');
|
||||
$role = new RoleForm(array(
|
||||
'onSuccess' => function (RoleForm $role) {
|
||||
$name = $role->getElement('name')->getValue();
|
||||
|
@ -88,9 +46,10 @@ class RolesController extends ActionController
|
|||
->setTitle($this->translate('New Role'))
|
||||
->setSubmitLabel($this->translate('Create Role'))
|
||||
->setIniConfig(Config::app('roles', true))
|
||||
->setRedirectUrl('roles')
|
||||
->setRedirectUrl('role/list')
|
||||
->handleRequest();
|
||||
$this->view->form = $role;
|
||||
$this->render('form');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,8 +57,9 @@ class RolesController extends ActionController
|
|||
*
|
||||
* @throws Zend_Controller_Action_Exception If the required parameter 'role' is missing or the role does not exist
|
||||
*/
|
||||
public function updateAction()
|
||||
public function editAction()
|
||||
{
|
||||
$this->assertPermission('config/authentication/roles/edit');
|
||||
$name = $this->_request->getParam('role');
|
||||
if (empty($name)) {
|
||||
throw new Zend_Controller_Action_Exception(
|
||||
|
@ -137,9 +97,10 @@ class RolesController extends ActionController
|
|||
}
|
||||
return false;
|
||||
})
|
||||
->setRedirectUrl('roles')
|
||||
->setRedirectUrl('role/list')
|
||||
->handleRequest();
|
||||
$this->view->form = $role;
|
||||
$this->render('form');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -149,6 +110,7 @@ class RolesController extends ActionController
|
|||
*/
|
||||
public function removeAction()
|
||||
{
|
||||
$this->assertPermission('config/authentication/roles/remove');
|
||||
$name = $this->_request->getParam('role');
|
||||
if (empty($name)) {
|
||||
throw new Zend_Controller_Action_Exception(
|
||||
|
@ -185,8 +147,9 @@ class RolesController extends ActionController
|
|||
$confirmation
|
||||
->setTitle(sprintf($this->translate('Remove Role %s'), $name))
|
||||
->setSubmitLabel($this->translate('Remove Role'))
|
||||
->setRedirectUrl('roles')
|
||||
->setRedirectUrl('role/list')
|
||||
->handleRequest();
|
||||
$this->view->form = $confirmation;
|
||||
$this->render('form');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Forms\Config\Authentication;
|
||||
namespace Icinga\Forms\Config\UserBackend;
|
||||
|
||||
use Exception;
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Data\ConfigObject;
|
||||
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
|
||||
{
|
||||
|
@ -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()
|
||||
*/
|
||||
public function onSuccess()
|
||||
{
|
||||
if (false === static::isValidAuthenticationBackend($this)) {
|
||||
if (false === static::isValidUserBackend($this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -103,12 +103,12 @@ class DbBackendForm extends Form
|
|||
*
|
||||
* @return bool Whether validation succeeded or not
|
||||
*/
|
||||
public static function isValidAuthenticationBackend(Form $form)
|
||||
public static function isValidUserBackend(Form $form)
|
||||
{
|
||||
try {
|
||||
$dbUserBackend = new DbUserBackend(ResourceFactory::createResource($form->getResourceConfig()));
|
||||
if ($dbUserBackend->count() < 1) {
|
||||
$form->addError($form->translate('No users found under the specified database backend'));
|
||||
if ($dbUserBackend->select()->where('is_active', true)->count() < 1) {
|
||||
$form->addError($form->translate('No active users found under the specified database backend'));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
|
@ -1,13 +1,13 @@
|
|||
<?php
|
||||
/* 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 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
|
||||
{
|
||||
|
@ -90,7 +90,7 @@ class ExternalBackendForm extends Form
|
|||
*
|
||||
* @return bool Whether validation succeeded or not
|
||||
*/
|
||||
public static function isValidAuthenticationBackend(Form $form)
|
||||
public static function isValidUserBackend(Form $form)
|
||||
{
|
||||
return true;
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Forms\Config\Authentication;
|
||||
namespace Icinga\Forms\Config\UserBackend;
|
||||
|
||||
use Exception;
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
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
|
||||
{
|
||||
|
@ -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()
|
||||
*/
|
||||
public function onSuccess()
|
||||
{
|
||||
if (false === static::isValidAuthenticationBackend($this)) {
|
||||
if (false === static::isValidUserBackend($this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -167,16 +167,11 @@ class LdapBackendForm extends Form
|
|||
*
|
||||
* @return bool Whether validation succeeded or not
|
||||
*/
|
||||
public static function isValidAuthenticationBackend(Form $form)
|
||||
public static function isValidUserBackend(Form $form)
|
||||
{
|
||||
try {
|
||||
$ldapUserBackend = new LdapUserBackend(
|
||||
ResourceFactory::createResource($form->getResourceConfig()),
|
||||
$form->getElement('user_class')->getValue(),
|
||||
$form->getElement('user_name_attribute')->getValue(),
|
||||
$form->getElement('base_dn')->getValue(),
|
||||
$form->getElement('filter')->getValue()
|
||||
);
|
||||
$ldapUserBackend = new LdapUserBackend(ResourceFactory::createResource($form->getResourceConfig()));
|
||||
$ldapUserBackend->setConfig(new ConfigObject($form->getValues()));
|
||||
$ldapUserBackend->assertAuthenticationPossible();
|
||||
} catch (AuthenticationException $e) {
|
||||
if (($previous = $e->getPrevious()) !== null) {
|
|
@ -11,11 +11,11 @@ use Icinga\Application\Platform;
|
|||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
use Icinga\Exception\ConfigurationError;
|
||||
use Icinga\Forms\Config\Authentication\DbBackendForm;
|
||||
use Icinga\Forms\Config\Authentication\LdapBackendForm;
|
||||
use Icinga\Forms\Config\Authentication\ExternalBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\DbBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\LdapBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
|
||||
|
||||
class AuthenticationBackendConfigForm extends ConfigForm
|
||||
class UserBackendConfigForm extends ConfigForm
|
||||
{
|
||||
/**
|
||||
* The available resources split by type
|
||||
|
@ -76,7 +76,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Add a particular authentication backend
|
||||
* Add a particular user backend
|
||||
*
|
||||
* The backend to add is identified by the array-key `name'.
|
||||
*
|
||||
|
@ -90,9 +90,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
{
|
||||
$name = isset($values['name']) ? $values['name'] : '';
|
||||
if (! $name) {
|
||||
throw new InvalidArgumentException($this->translate('Authentication backend name missing'));
|
||||
throw new InvalidArgumentException($this->translate('User backend name missing'));
|
||||
} elseif ($this->config->hasSection($name)) {
|
||||
throw new InvalidArgumentException($this->translate('Authentication backend already exists'));
|
||||
throw new InvalidArgumentException($this->translate('User backend already exists'));
|
||||
}
|
||||
|
||||
unset($values['name']);
|
||||
|
@ -101,7 +101,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Edit a particular authentication backend
|
||||
* Edit a particular user backend
|
||||
*
|
||||
* @param string $name The name of the backend to edit
|
||||
* @param array $values The values to edit the configuration with
|
||||
|
@ -113,11 +113,11 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
public function edit($name, array $values)
|
||||
{
|
||||
if (! $name) {
|
||||
throw new InvalidArgumentException($this->translate('Old authentication backend name missing'));
|
||||
throw new InvalidArgumentException($this->translate('Old user backend name missing'));
|
||||
} elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) {
|
||||
throw new InvalidArgumentException($this->translate('New authentication backend name missing'));
|
||||
throw new InvalidArgumentException($this->translate('New user backend name missing'));
|
||||
} elseif (! $this->config->hasSection($name)) {
|
||||
throw new InvalidArgumentException($this->translate('Unknown authentication backend provided'));
|
||||
throw new InvalidArgumentException($this->translate('Unknown user backend provided'));
|
||||
}
|
||||
|
||||
$backendConfig = $this->config->getSection($name);
|
||||
|
@ -132,7 +132,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Remove the given authentication backend
|
||||
* Remove the given user backend
|
||||
*
|
||||
* @param string $name The name of the backend to remove
|
||||
*
|
||||
|
@ -143,9 +143,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
public function remove($name)
|
||||
{
|
||||
if (! $name) {
|
||||
throw new InvalidArgumentException($this->translate('Authentication backend name missing'));
|
||||
throw new InvalidArgumentException($this->translate('user backend name missing'));
|
||||
} elseif (! $this->config->hasSection($name)) {
|
||||
throw new InvalidArgumentException($this->translate('Unknown authentication backend provided'));
|
||||
throw new InvalidArgumentException($this->translate('Unknown user backend provided'));
|
||||
}
|
||||
|
||||
$backendConfig = $this->config->getSection($name);
|
||||
|
@ -154,7 +154,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Move the given authentication backend up or down in order
|
||||
* Move the given user backend up or down in order
|
||||
*
|
||||
* @param string $name The name of the backend to be moved
|
||||
* @param int $position The new (absolute) position of the backend
|
||||
|
@ -166,9 +166,9 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
public function move($name, $position)
|
||||
{
|
||||
if (! $name) {
|
||||
throw new InvalidArgumentException($this->translate('Authentication backend name missing'));
|
||||
throw new InvalidArgumentException($this->translate('User backend name missing'));
|
||||
} elseif (! $this->config->hasSection($name)) {
|
||||
throw new InvalidArgumentException($this->translate('Unknown authentication backend provided'));
|
||||
throw new InvalidArgumentException($this->translate('Unknown user backend provided'));
|
||||
}
|
||||
|
||||
$backendOrder = $this->config->keys();
|
||||
|
@ -186,7 +186,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Add or edit an authentication backend and save the configuration
|
||||
* Add or edit an user backend and save the configuration
|
||||
*
|
||||
* Performs a connectivity validation using the submitted values. A checkbox is
|
||||
* added to the form to skip the check if it fails and redirection is aborted.
|
||||
|
@ -197,20 +197,20 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
{
|
||||
if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
|
||||
$backendForm = $this->getBackendForm($this->getElement('type')->getValue());
|
||||
if (false === $backendForm::isValidAuthenticationBackend($this)) {
|
||||
if (false === $backendForm::isValidUserBackend($this)) {
|
||||
$this->addElement($this->getForceCreationCheckbox());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$authBackend = $this->request->getQuery('auth_backend');
|
||||
$authBackend = $this->request->getQuery('backend');
|
||||
try {
|
||||
if ($authBackend === null) { // create new backend
|
||||
$this->add($this->getValues());
|
||||
$message = $this->translate('Authentication backend "%s" has been successfully created');
|
||||
$message = $this->translate('User backend "%s" has been successfully created');
|
||||
} else { // edit existing backend
|
||||
$this->edit($authBackend, $this->getValues());
|
||||
$message = $this->translate('Authentication backend "%s" has been successfully changed');
|
||||
$message = $this->translate('User backend "%s" has been successfully changed');
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
Notification::error($e->getMessage());
|
||||
|
@ -225,7 +225,7 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Populate the form in case an authentication backend is being edited
|
||||
* Populate the form in case an user backend is being edited
|
||||
*
|
||||
* @see Form::onRequest()
|
||||
*
|
||||
|
@ -233,12 +233,12 @@ class AuthenticationBackendConfigForm extends ConfigForm
|
|||
*/
|
||||
public function onRequest()
|
||||
{
|
||||
$authBackend = $this->request->getQuery('auth_backend');
|
||||
$authBackend = $this->request->getQuery('backend');
|
||||
if ($authBackend !== null) {
|
||||
if ($authBackend === '') {
|
||||
throw new ConfigurationError($this->translate('Authentication backend name missing'));
|
||||
throw new ConfigurationError($this->translate('User backend name missing'));
|
||||
} elseif (! $this->config->hasSection($authBackend)) {
|
||||
throw new ConfigurationError($this->translate('Unknown authentication backend provided'));
|
||||
throw new ConfigurationError($this->translate('Unknown user backend provided'));
|
||||
} elseif ($this->config->getSection($authBackend)->backend === null) {
|
||||
throw new ConfigurationError(
|
||||
sprintf($this->translate('Backend "%s" has no `backend\' setting'), $authBackend)
|
|
@ -7,7 +7,7 @@ use InvalidArgumentException;
|
|||
use Icinga\Web\Notification;
|
||||
use Icinga\Forms\ConfigForm;
|
||||
|
||||
class AuthenticationBackendReorderForm extends ConfigForm
|
||||
class UserBackendReorderForm extends ConfigForm
|
||||
{
|
||||
/**
|
||||
* Initialize this form
|
||||
|
@ -38,7 +38,7 @@ class AuthenticationBackendReorderForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the authentication backend order and save the configuration
|
||||
* Update the user backend order and save the configuration
|
||||
*
|
||||
* @see Form::onSuccess()
|
||||
*/
|
||||
|
@ -62,13 +62,13 @@ class AuthenticationBackendReorderForm extends ConfigForm
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the config form for authentication backends
|
||||
* Return the config form for user backends
|
||||
*
|
||||
* @return ConfigForm
|
||||
*/
|
||||
protected function getConfigForm()
|
||||
{
|
||||
$form = new AuthenticationBackendConfigForm();
|
||||
$form = new UserBackendConfigForm();
|
||||
$form->setIniConfig($this->config);
|
||||
return $form;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -25,9 +25,25 @@ class RoleForm extends ConfigForm
|
|||
'config/*' => 'config/*',
|
||||
'config/application/*' => 'config/application/*',
|
||||
'config/application/general' => 'config/application/general',
|
||||
'config/application/authentication' => 'config/application/authentication',
|
||||
'config/application/resources' => 'config/application/resources',
|
||||
'config/application/roles' => 'config/application/roles',
|
||||
'config/application/userbackend' => 'config/application/userbackend',
|
||||
'config/application/usergroupbackend' => 'config/application/usergroupbackend',
|
||||
'config/authentication/*' => 'config/authentication/*',
|
||||
'config/authentication/users/*' => 'config/authentication/users/*',
|
||||
'config/authentication/users/show' => 'config/authentication/users/show',
|
||||
'config/authentication/users/add' => 'config/authentication/users/add',
|
||||
'config/authentication/users/edit' => 'config/authentication/users/edit',
|
||||
'config/authentication/users/remove' => 'config/authentication/users/remove',
|
||||
'config/authentication/groups/*' => 'config/authentication/groups/*',
|
||||
'config/authentication/groups/show' => 'config/authentication/groups/show',
|
||||
'config/authentication/groups/add' => 'config/authentication/groups/add',
|
||||
'config/authentication/groups/edit' => 'config/authentication/groups/edit',
|
||||
'config/authentication/groups/remove' => 'config/authentication/groups/remove',
|
||||
'config/authentication/roles/*' => 'config/authentication/roles/*',
|
||||
'config/authentication/roles/show' => 'config/authentication/roles/show',
|
||||
'config/authentication/roles/add' => 'config/authentication/roles/add',
|
||||
'config/authentication/roles/edit' => 'config/authentication/roles/edit',
|
||||
'config/authentication/roles/remove' => 'config/authentication/roles/remove',
|
||||
'config/modules' => 'config/modules'
|
||||
);
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<?= $tabs; ?>
|
||||
</div>
|
||||
<div class="content" data-base-target="_next">
|
||||
<a href="<?= $this->href('/config/createAuthenticationBackend'); ?>">
|
||||
<?= $this->icon('plus'); ?><?= $this->translate('Create A New Authentication Backend'); ?>
|
||||
<a href="<?= $this->href('/config/createuserbackend'); ?>">
|
||||
<?= $this->icon('plus'); ?><?= $this->translate('Create A New User Backend'); ?>
|
||||
</a>
|
||||
<div id="authentication-reorder-form">
|
||||
<?= $form; ?>
|
|
@ -12,22 +12,22 @@
|
|||
<td class="action">
|
||||
<?= $this->qlink(
|
||||
$backendNames[$i],
|
||||
'config/editAuthenticationBackend',
|
||||
array('auth_backend' => $backendNames[$i]),
|
||||
'config/edituserbackend',
|
||||
array('backend' => $backendNames[$i]),
|
||||
array(
|
||||
'icon' => 'edit',
|
||||
'title' => sprintf($this->translate('Edit authentication backend %s'), $backendNames[$i])
|
||||
'title' => sprintf($this->translate('Edit user backend %s'), $backendNames[$i])
|
||||
)
|
||||
); ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= $this->qlink(
|
||||
'',
|
||||
'config/removeAuthenticationBackend',
|
||||
array('auth_backend' => $backendNames[$i]),
|
||||
'config/removeuserbackend',
|
||||
array('backend' => $backendNames[$i]),
|
||||
array(
|
||||
'icon' => 'trash',
|
||||
'title' => sprintf($this->translate('Remove authentication backend %s'), $backendNames[$i])
|
||||
'title' => sprintf($this->translate('Remove user backend %s'), $backendNames[$i])
|
||||
)
|
||||
); ?>
|
||||
</td>
|
||||
|
@ -40,7 +40,7 @@
|
|||
); ?>" title="<?= $this->translate(
|
||||
'Move up in authentication order'
|
||||
); ?>" aria-label="<?= sprintf(
|
||||
$this->translate('Move authentication backend %s upwards'),
|
||||
$this->translate('Move user backend %s upwards'),
|
||||
$backendNames[$i]
|
||||
); ?>">
|
||||
<?= $this->icon('up-big'); ?>
|
||||
|
@ -54,7 +54,7 @@
|
|||
); ?>" title="<?= $this->translate(
|
||||
'Move down in authentication order'
|
||||
); ?>" aria-label="<?= sprintf(
|
||||
$this->translate('Move authentication backend %s downwards'),
|
||||
$this->translate('Move user backend %s downwards'),
|
||||
$backendNames[$i]
|
||||
); ?>">
|
||||
<?= $this->icon('down-big'); ?>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="controls">
|
||||
<?= $tabs->showOnlyCloseButton() ?>
|
||||
<?= $tabs->showOnlyCloseButton(); ?>
|
||||
</div>
|
||||
<div class="content">
|
||||
<?= $form ?>
|
||||
<?= $form; ?>
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,6 +1,6 @@
|
|||
<div class="controls">
|
||||
<?= $tabs->showOnlyCloseButton() ?>
|
||||
<?= $tabs->showOnlyCloseButton(); ?>
|
||||
</div>
|
||||
<div class="content">
|
||||
<?= $form ?>
|
||||
<?= $form; ?>
|
||||
</div>
|
|
@ -22,7 +22,7 @@
|
|||
<td>
|
||||
<?= $this->qlink(
|
||||
$name,
|
||||
'roles/update',
|
||||
'role/edit',
|
||||
array('role' => $name),
|
||||
array('title' => sprintf($this->translate('Edit role %s'), $name))
|
||||
); ?>
|
||||
|
@ -54,7 +54,7 @@
|
|||
<td>
|
||||
<?= $this->qlink(
|
||||
'',
|
||||
'roles/remove',
|
||||
'role/remove',
|
||||
array('role' => $name),
|
||||
array(
|
||||
'icon' => 'trash',
|
||||
|
@ -67,7 +67,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
<?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') ?>
|
||||
</a>
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
<div class="controls">
|
||||
<?= $tabs->showOnlyCloseButton() ?>
|
||||
<?= $tabs->showOnlyCloseButton(); ?>
|
||||
</div>
|
||||
<div class="content">
|
||||
<?= $form ?>
|
||||
<?= $form; ?>
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="controls">
|
||||
<?= $tabs->showOnlyCloseButton(); ?>
|
||||
</div>
|
||||
<div class="content">
|
||||
<?= $form; ?>
|
||||
</div>
|
|
@ -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>
|
|
@ -1,21 +1,25 @@
|
|||
# Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+
|
||||
|
||||
CREATE TABLE `icingaweb_group`(
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`parent` varchar(64) COLLATE utf8_unicode_ci NULL DEFAULT NULL,
|
||||
`parent` int(10) unsigned NULL DEFAULT NULL,
|
||||
`ctime` timestamp NULL DEFAULT NULL,
|
||||
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`name`)
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_name` (`name`),
|
||||
CONSTRAINT `fk_icingaweb_group_parent_id` FOREIGN KEY (`parent`)
|
||||
REFERENCES `icingaweb_group` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE `icingaweb_group_membership`(
|
||||
`group_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`group_id` int(10) unsigned NOT NULL,
|
||||
`username` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`ctime` timestamp NULL DEFAULT NULL,
|
||||
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`group_name`,`username`),
|
||||
CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_name`)
|
||||
REFERENCES `icingaweb_group` (`name`)
|
||||
PRIMARY KEY (`group_id`,`username`),
|
||||
CONSTRAINT `fk_icingaweb_group_membership_icingaweb_group` FOREIGN KEY (`group_id`)
|
||||
REFERENCES `icingaweb_group` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE `icingaweb_user`(
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
|
||||
|
||||
CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
|
||||
SELECT EXTRACT(EPOCH FROM $1)::bigint AS result
|
||||
' LANGUAGE sql;
|
||||
|
||||
CREATE TABLE "icingaweb_group" (
|
||||
"id" serial,
|
||||
"name" character varying(64) NOT NULL,
|
||||
"parent" character varying(64) NULL DEFAULT NULL,
|
||||
"parent" int NULL DEFAULT NULL,
|
||||
"ctime" timestamp NULL DEFAULT NULL,
|
||||
"mtime" timestamp NULL DEFAULT NULL
|
||||
);
|
||||
|
@ -10,7 +15,7 @@ CREATE TABLE "icingaweb_group" (
|
|||
ALTER TABLE ONLY "icingaweb_group"
|
||||
ADD CONSTRAINT pk_icingaweb_group
|
||||
PRIMARY KEY (
|
||||
"name"
|
||||
"id"
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_icingaweb_group
|
||||
|
@ -19,8 +24,17 @@ CREATE UNIQUE INDEX idx_icingaweb_group
|
|||
lower((name)::text)
|
||||
);
|
||||
|
||||
ALTER TABLE ONLY "icingaweb_group"
|
||||
ADD CONSTRAINT fk_icingaweb_group_parent_id
|
||||
FOREIGN KEY (
|
||||
"parent"
|
||||
)
|
||||
REFERENCES "icingaweb_group" (
|
||||
"id"
|
||||
);
|
||||
|
||||
CREATE TABLE "icingaweb_group_membership" (
|
||||
"group_name" character varying(64) NOT NULL,
|
||||
"group_id" int NOT NULL,
|
||||
"username" character varying(64) NOT NULL,
|
||||
"ctime" timestamp NULL DEFAULT NULL,
|
||||
"mtime" timestamp NULL DEFAULT NULL
|
||||
|
@ -28,15 +42,17 @@ CREATE TABLE "icingaweb_group_membership" (
|
|||
|
||||
ALTER TABLE ONLY "icingaweb_group_membership"
|
||||
ADD CONSTRAINT pk_icingaweb_group_membership
|
||||
PRIMARY KEY (
|
||||
"group_name",
|
||||
"username"
|
||||
FOREIGN KEY (
|
||||
"group_id"
|
||||
)
|
||||
REFERENCES "icingaweb_group" (
|
||||
"id"
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_icingaweb_group_membership
|
||||
ON "icingaweb_group_membership"
|
||||
USING btree (
|
||||
lower((group_name)::text),
|
||||
group_id,
|
||||
lower((username)::text)
|
||||
);
|
||||
|
||||
|
|
|
@ -9,13 +9,15 @@ use LogicException;
|
|||
use UnexpectedValueException;
|
||||
use Icinga\Util\File;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Data\Selectable;
|
||||
use Icinga\Data\SimpleQuery;
|
||||
use Icinga\File\Ini\IniWriter;
|
||||
use Icinga\Exception\NotReadableError;
|
||||
|
||||
/**
|
||||
* Container for INI like configuration and global registry of application and module related configuration.
|
||||
*/
|
||||
class Config implements Countable, Iterator
|
||||
class Config implements Countable, Iterator, Selectable
|
||||
{
|
||||
/**
|
||||
* Configuration directory where ALL (application and module) configuration is located
|
||||
|
@ -85,6 +87,26 @@ class Config implements Countable, Iterator
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the internal ConfigObject
|
||||
*
|
||||
* @return ConfigObject
|
||||
*/
|
||||
public function getConfigObject()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a query for the internal config object
|
||||
*
|
||||
* @return SimpleQuery
|
||||
*/
|
||||
public function select()
|
||||
{
|
||||
return $this->config->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the count of available sections
|
||||
*
|
||||
|
@ -92,7 +114,7 @@ class Config implements Countable, Iterator
|
|||
*/
|
||||
public function count()
|
||||
{
|
||||
return $this->config->count();
|
||||
return $this->select()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -243,7 +243,9 @@ class Logger
|
|||
return vsprintf(
|
||||
array_shift($arguments),
|
||||
array_map(
|
||||
function ($a) { return is_string($a) ? $a : json_encode($a); },
|
||||
function ($a) {
|
||||
return is_string($a) ? $a : ($a instanceof Exception ? $a->getMessage() : json_encode($a));
|
||||
},
|
||||
$arguments
|
||||
)
|
||||
);
|
||||
|
|
|
@ -179,10 +179,26 @@ class Module
|
|||
protected $paneItems = array();
|
||||
|
||||
/**
|
||||
* A set of objects representing a searchUrl configuration
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $searchUrls = array();
|
||||
|
||||
/**
|
||||
* This module's user backends providing several authentication mechanisms
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $userBackends = array();
|
||||
|
||||
/**
|
||||
* This module's user group backends
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $userGroupBackends = array();
|
||||
|
||||
/**
|
||||
* Provide a search URL
|
||||
*
|
||||
|
@ -201,6 +217,11 @@ class Module
|
|||
$this->searchUrls[] = $searchUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this module's search urls
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSearchUrls()
|
||||
{
|
||||
$this->launchConfigScript();
|
||||
|
@ -702,6 +723,28 @@ class Module
|
|||
return new $this->setupWizard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this module's user backends
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUserBackends()
|
||||
{
|
||||
$this->launchConfigScript();
|
||||
return $this->userBackends;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this module's user group backends
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUserGroupBackends()
|
||||
{
|
||||
$this->launchConfigScript();
|
||||
return $this->userGroupBackends;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a named permission
|
||||
*
|
||||
|
@ -777,6 +820,34 @@ class Module
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a user backend capable of authenticating users
|
||||
*
|
||||
* @param string $identifier The identifier of the new backend type
|
||||
* @param string $className The name of the class
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function provideUserBackend($identifier, $className)
|
||||
{
|
||||
$this->userBackends[strtolower($identifier)] = $className;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a user group backend
|
||||
*
|
||||
* @param string $identifier The identifier of the new backend type
|
||||
* @param string $className The name of the class
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function provideUserGroupBackend($identifier, $className)
|
||||
{
|
||||
$this->userGroupBackends[strtolower($identifier)] = $className;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register new namespaces on the autoloader
|
||||
*
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Icinga\Authentication;
|
|||
|
||||
use Iterator;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Authentication\User\UserBackend;
|
||||
use Icinga\Authentication\User\UserBackendInterface;
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Exception\ConfigurationError;
|
||||
|
@ -24,7 +26,7 @@ class AuthChain implements Iterator
|
|||
/**
|
||||
* The consecutive user backend while looping
|
||||
*
|
||||
* @var UserBackend
|
||||
* @var UserBackendInterface
|
||||
*/
|
||||
private $currentBackend;
|
||||
|
||||
|
@ -52,7 +54,7 @@ class AuthChain implements Iterator
|
|||
/**
|
||||
* Return the current user backend
|
||||
*
|
||||
* @return UserBackend
|
||||
* @return UserBackendInterface
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
namespace Icinga\Authentication;
|
||||
|
||||
use Exception;
|
||||
use Icinga\Authentication\UserGroup\UserGroupBackend;
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Exception\IcingaException;
|
||||
use Icinga\Exception\NotReadableError;
|
||||
|
@ -55,7 +56,7 @@ class Manager
|
|||
} catch (NotReadableError $e) {
|
||||
Logger::error(
|
||||
new IcingaException(
|
||||
'Cannot load preferences for user "%s". An exception was thrown',
|
||||
'Cannot load preferences for user "%s". An exception was thrown: %s',
|
||||
$username,
|
||||
$e
|
||||
)
|
||||
|
@ -73,7 +74,7 @@ class Manager
|
|||
} catch (Exception $e) {
|
||||
Logger::error(
|
||||
new IcingaException(
|
||||
'Cannot load preferences for user "%s". An exception was thrown',
|
||||
'Cannot load preferences for user "%s". An exception was thrown: %s',
|
||||
$username,
|
||||
$e
|
||||
)
|
||||
|
@ -91,7 +92,7 @@ class Manager
|
|||
$groupsFromBackend = $groupBackend->getMemberships($user);
|
||||
} catch (Exception $e) {
|
||||
Logger::error(
|
||||
'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown:',
|
||||
'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s',
|
||||
$username,
|
||||
$name,
|
||||
$e
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -1,23 +1,29 @@
|
|||
<?php
|
||||
/* 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\User;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $stripUsernameRegexp;
|
||||
protected $stripUsernameRegexp;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the given user
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $password
|
||||
*
|
||||
* @return bool
|
||||
* @return bool True on success, false on failure
|
||||
*
|
||||
* @throws AuthenticationException In case authentication is not possible due to an error
|
||||
*/
|
||||
public function hasUser(User $user)
|
||||
public function authenticate(User $user, $password = null)
|
||||
{
|
||||
if (isset($_SERVER['REMOTE_USER'])) {
|
||||
$username = $_SERVER['REMOTE_USER'];
|
||||
$user->setRemoteUserInformation($username, 'REMOTE_USER');
|
||||
|
||||
if ($this->stripUsernameRegexp) {
|
||||
$stripped = preg_replace($this->stripUsernameRegexp, '', $username);
|
||||
if ($stripped !== false) {
|
||||
|
@ -61,23 +82,11 @@ class ExternalBackend extends UserBackend
|
|||
$username = $stripped;
|
||||
}
|
||||
}
|
||||
|
||||
$user->setUsername($username);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $password
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authenticate(User $user, $password = null)
|
||||
{
|
||||
return $this->hasUser($user);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -4,22 +4,15 @@
|
|||
namespace Icinga\Data;
|
||||
|
||||
use Iterator;
|
||||
use Countable;
|
||||
use ArrayAccess;
|
||||
use LogicException;
|
||||
use Icinga\Data\DataArray\ArrayDatasource;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
|
||||
/**
|
||||
* Container for configuration values
|
||||
*/
|
||||
class ConfigObject implements Countable, Iterator, ArrayAccess
|
||||
class ConfigObject extends ArrayDatasource implements Iterator, ArrayAccess
|
||||
{
|
||||
/**
|
||||
* This config's data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new config
|
||||
*
|
||||
|
@ -27,15 +20,14 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
|
|||
*/
|
||||
public function __construct(array $data = array())
|
||||
{
|
||||
$this->data = array();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
// Convert all embedded arrays to ConfigObjects as well
|
||||
foreach ($data as & $value) {
|
||||
if (is_array($value)) {
|
||||
$this->data[$key] = new static($value);
|
||||
} else {
|
||||
$this->data[$key] = $value;
|
||||
$value = new static($value);
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,16 +47,6 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
|
|||
$this->data = $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the count of available sections and properties
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current position of $this->data
|
||||
*
|
||||
|
@ -197,7 +179,7 @@ class ConfigObject implements Countable, Iterator, ArrayAccess
|
|||
public function offsetSet($key, $value)
|
||||
{
|
||||
if ($key === null) {
|
||||
throw new LogicException('Appending values without an explicit key is not supported');
|
||||
throw new ProgrammingError('Appending values without an explicit key is not supported');
|
||||
}
|
||||
|
||||
$this->$key = $value;
|
||||
|
|
|
@ -3,27 +3,78 @@
|
|||
|
||||
namespace Icinga\Data\DataArray;
|
||||
|
||||
use ArrayIterator;
|
||||
use Icinga\Data\Selectable;
|
||||
use Icinga\Data\SimpleQuery;
|
||||
|
||||
class ArrayDatasource implements Selectable
|
||||
{
|
||||
/**
|
||||
* The array being used as data source
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* The current result
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Constructor, create a new Datasource for the given Array
|
||||
* The result of a counted query
|
||||
*
|
||||
* @param array $array The array you're going to use as a data source
|
||||
* @var int
|
||||
*/
|
||||
public function __construct(array $array)
|
||||
protected $count;
|
||||
|
||||
/**
|
||||
* The name of the column to map array keys on
|
||||
*
|
||||
* In case the array being used as data source provides keys of type string,this name
|
||||
* will be used to set such as column on each row, if the column is not set already.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $keyColumn;
|
||||
|
||||
/**
|
||||
* Create a new data source for the given array
|
||||
*
|
||||
* @param array $data The array you're going to use as a data source
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = (array) $array;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a Query object
|
||||
* Set the name of the column to map array keys on
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
|
@ -32,6 +83,25 @@ class ArrayDatasource implements Selectable
|
|||
return new SimpleQuery($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and return all rows of the given query's result set using an iterator
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return ArrayIterator
|
||||
*/
|
||||
public function query(SimpleQuery $query)
|
||||
{
|
||||
return new ArrayIterator($this->fetchAll($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and return a column of all rows of the result set as an array
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchColumn(SimpleQuery $query)
|
||||
{
|
||||
$result = array();
|
||||
|
@ -39,9 +109,17 @@ class ArrayDatasource implements Selectable
|
|||
$arr = (array) $row;
|
||||
$result[] = array_shift($arr);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and return all rows of the given query's result as a flattened key/value based array
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs(SimpleQuery $query)
|
||||
{
|
||||
$result = array();
|
||||
|
@ -53,104 +131,164 @@ class ArrayDatasource implements Selectable
|
|||
$keys[1] = $keys[0];
|
||||
}
|
||||
}
|
||||
|
||||
$result[$row->{$keys[0]}] = $row->{$keys[1]};
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and return the first row of the given query's result
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return object|false The row or false in case the result is empty
|
||||
*/
|
||||
public function fetchRow(SimpleQuery $query)
|
||||
{
|
||||
$result = $this->getResult($query);
|
||||
if (empty($result)) {
|
||||
return false;
|
||||
}
|
||||
return $result[0];
|
||||
|
||||
return array_shift($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and return all rows of the given query's result as an array
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAll(SimpleQuery $query)
|
||||
{
|
||||
return $this->getResult($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all rows of the given query's result
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(SimpleQuery $query)
|
||||
{
|
||||
$this->createResult($query);
|
||||
return count($this->result);
|
||||
if ($this->count === null) {
|
||||
$this->count = count($this->createResult($query));
|
||||
}
|
||||
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return the result for the given query
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function createResult(SimpleQuery $query)
|
||||
{
|
||||
if ($this->hasResult()) {
|
||||
return $this;
|
||||
}
|
||||
$result = array();
|
||||
|
||||
$columns = $query->getColumns();
|
||||
$filter = $query->getFilter();
|
||||
foreach ($this->data as & $row) {
|
||||
$offset = $query->hasOffset() ? $query->getOffset() : 0;
|
||||
$limit = $query->hasLimit() ? $query->getLimit() : 0;
|
||||
|
||||
$foundStringKey = false;
|
||||
$result = array();
|
||||
$skipped = 0;
|
||||
foreach ($this->data as $key => $row) {
|
||||
if (is_string($key) && $this->keyColumn !== null && !isset($row->{$this->keyColumn})) {
|
||||
$row = clone $row; // Make sure that this won't affect the actual data
|
||||
$row->{$this->keyColumn} = $key;
|
||||
}
|
||||
|
||||
if (! $filter->matches($row)) {
|
||||
continue;
|
||||
} elseif ($skipped < $offset) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get only desired columns if asked so
|
||||
if (empty($columns)) {
|
||||
$result[] = $row;
|
||||
if (! empty($columns)) {
|
||||
$filteredRow = (object) array();
|
||||
foreach ($columns as $alias => $name) {
|
||||
if (! is_string($alias)) {
|
||||
$alias = $name;
|
||||
}
|
||||
|
||||
if (isset($row->$name)) {
|
||||
$filteredRow->$alias = $row->$name;
|
||||
} else {
|
||||
$c_row = (object) array();
|
||||
foreach ($columns as $alias => $key) {
|
||||
if (is_int($alias)) {
|
||||
$alias = $key;
|
||||
$filteredRow->$alias = null;
|
||||
}
|
||||
}
|
||||
if (isset($row->$key)) {
|
||||
$c_row->$alias = $row->$key;
|
||||
} else {
|
||||
$c_row->$alias = null;
|
||||
$filteredRow = $row;
|
||||
}
|
||||
}
|
||||
$result[] = $c_row;
|
||||
|
||||
$foundStringKey |= is_string($key);
|
||||
$result[$key] = $filteredRow;
|
||||
|
||||
if (count($result) === $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the result
|
||||
|
||||
if ($query->hasOrder()) {
|
||||
if ($foundStringKey) {
|
||||
uasort($result, array($query, 'compare'));
|
||||
} else {
|
||||
usort($result, array($query, 'compare'));
|
||||
}
|
||||
|
||||
$this->setResult($result);
|
||||
return $this;
|
||||
} elseif (! $foundStringKey) {
|
||||
$result = array_values($result);
|
||||
}
|
||||
|
||||
protected function getLimitedResult($query)
|
||||
{
|
||||
if ($query->hasLimit()) {
|
||||
if ($query->hasOffset()) {
|
||||
$offset = $query->getOffset();
|
||||
} else {
|
||||
$offset = 0;
|
||||
}
|
||||
return array_slice($this->result, $offset, $query->getLimit());
|
||||
} else {
|
||||
return $this->result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether a query result exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasResult()
|
||||
{
|
||||
return $this->result !== null;
|
||||
}
|
||||
|
||||
protected function setResult($result)
|
||||
/**
|
||||
* Set the current result
|
||||
*
|
||||
* @param array $result
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function setResult(array $result)
|
||||
{
|
||||
return $this->result = $result;
|
||||
$this->result = $result;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the result for the given query
|
||||
*
|
||||
* @param SimpleQuery $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getResult(SimpleQuery $query)
|
||||
{
|
||||
if (! $this->hasResult()) {
|
||||
$this->createResult($query);
|
||||
$this->setResult($this->createResult($query));
|
||||
}
|
||||
return $this->getLimitedResult($query);
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,26 @@
|
|||
namespace Icinga\Data\Db;
|
||||
|
||||
use PDO;
|
||||
use Iterator;
|
||||
use Zend_Db;
|
||||
use Icinga\Application\Benchmark;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Data\Db\DbQuery;
|
||||
use Icinga\Data\Extensible;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Data\Filter\FilterAnd;
|
||||
use Icinga\Data\Filter\FilterNot;
|
||||
use Icinga\Data\Filter\FilterOr;
|
||||
use Icinga\Data\Reducible;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
use Icinga\Data\Selectable;
|
||||
use Icinga\Data\Updatable;
|
||||
use Icinga\Exception\ConfigurationError;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
|
||||
/**
|
||||
* Encapsulate database connections and query creation
|
||||
*/
|
||||
class DbConnection implements Selectable
|
||||
class DbConnection implements Selectable, Extensible, Updatable, Reducible
|
||||
{
|
||||
/**
|
||||
* Connection config
|
||||
|
@ -72,13 +80,25 @@ class DbConnection implements Selectable
|
|||
/**
|
||||
* Provide a query on this connection
|
||||
*
|
||||
* @return Query
|
||||
* @return DbQuery
|
||||
*/
|
||||
public function select()
|
||||
{
|
||||
return new DbQuery($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and return all rows of the given query's result set using an iterator
|
||||
*
|
||||
* @param DbQuery $query
|
||||
*
|
||||
* @return Iterator
|
||||
*/
|
||||
public function query(DbQuery $query)
|
||||
{
|
||||
return $query->getSelectQuery()->query();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for database type
|
||||
*
|
||||
|
@ -191,6 +211,18 @@ class DbConnection implements Selectable
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all rows of the result set
|
||||
*
|
||||
* @param DbQuery $query
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(DbQuery $query)
|
||||
{
|
||||
return (int) $this->dbAdapter->fetchOne($query->getCountQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an array containing all rows of the result set
|
||||
*
|
||||
|
@ -200,10 +232,7 @@ class DbConnection implements Selectable
|
|||
*/
|
||||
public function fetchAll(DbQuery $query)
|
||||
{
|
||||
Benchmark::measure('DB is fetching All');
|
||||
$result = $this->dbAdapter->fetchAll($query->getSelectQuery());
|
||||
Benchmark::measure('DB fetch done');
|
||||
return $result;
|
||||
return $this->dbAdapter->fetchAll($query->getSelectQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -215,21 +244,17 @@ class DbConnection implements Selectable
|
|||
*/
|
||||
public function fetchRow(DbQuery $query)
|
||||
{
|
||||
Benchmark::measure('DB is fetching row');
|
||||
$result = $this->dbAdapter->fetchRow($query->getSelectQuery());
|
||||
Benchmark::measure('DB row done');
|
||||
return $result;
|
||||
return $this->dbAdapter->fetchRow($query->getSelectQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a column of all rows of the result set as an array
|
||||
* Fetch the first column of all rows of the result set as an array
|
||||
*
|
||||
* @param DbQuery $query
|
||||
* @param int $columnIndex Index of the column to fetch
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchColumn(DbQuery $query, $columnIndex = 0)
|
||||
public function fetchColumn(DbQuery $query)
|
||||
{
|
||||
return $this->dbAdapter->fetchCol($query->getSelectQuery());
|
||||
}
|
||||
|
@ -259,4 +284,158 @@ class DbConnection implements Selectable
|
|||
{
|
||||
return $this->dbAdapter->fetchPairs($query->getSelectQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a table row with the given data
|
||||
*
|
||||
* Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
|
||||
* as third parameter $types to define a different type than string for a particular column.
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $bind
|
||||
* @param array $types
|
||||
*
|
||||
* @return int The number of affected rows
|
||||
*/
|
||||
public function insert($table, array $bind, array $types = array())
|
||||
{
|
||||
$values = array();
|
||||
foreach ($bind as $column => $_) {
|
||||
$values[] = ':' . $column;
|
||||
}
|
||||
|
||||
$sql = 'INSERT INTO ' . $table
|
||||
. ' (' . join(', ', array_keys($bind)) . ') '
|
||||
. 'VALUES (' . join(', ', $values) . ')';
|
||||
$statement = $this->dbAdapter->prepare($sql);
|
||||
|
||||
foreach ($bind as $column => $value) {
|
||||
$type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
|
||||
$statement->bindValue(':' . $column, $value, $type);
|
||||
}
|
||||
|
||||
$statement->execute();
|
||||
return $statement->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update table rows with the given data, optionally limited by using a filter
|
||||
*
|
||||
* Pass an array with a column name (the same as in $bind) and a PDO::PARAM_* constant as value
|
||||
* as fourth parameter $types to define a different type than string for a particular column.
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $bind
|
||||
* @param Filter $filter
|
||||
* @param array $types
|
||||
*
|
||||
* @return int The number of affected rows
|
||||
*/
|
||||
public function update($table, array $bind, Filter $filter = null, array $types = array())
|
||||
{
|
||||
$set = array();
|
||||
foreach ($bind as $column => $_) {
|
||||
$set[] = $column . ' = :' . $column;
|
||||
}
|
||||
|
||||
$sql = 'UPDATE ' . $table
|
||||
. ' SET ' . join(', ', $set)
|
||||
. ($filter ? ' WHERE ' . $this->renderFilter($filter) : '');
|
||||
$statement = $this->dbAdapter->prepare($sql);
|
||||
|
||||
foreach ($bind as $column => $value) {
|
||||
$type = isset($types[$column]) ? $types[$column] : PDO::PARAM_STR;
|
||||
$statement->bindValue(':' . $column, $value, $type);
|
||||
}
|
||||
|
||||
$statement->execute();
|
||||
return $statement->rowCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete table rows, optionally limited by using a filter
|
||||
*
|
||||
* @param string $table
|
||||
* @param Filter $filter
|
||||
*
|
||||
* @return int The number of affected rows
|
||||
*/
|
||||
public function delete($table, Filter $filter = null)
|
||||
{
|
||||
return $this->dbAdapter->delete($table, $filter ? $this->renderFilter($filter) : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render and return the given filter as SQL-WHERE clause
|
||||
*
|
||||
* @param Filter $filter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderFilter(Filter $filter, $level = 0)
|
||||
{
|
||||
// TODO: This is supposed to supersede DbQuery::renderFilter()
|
||||
$where = '';
|
||||
if ($filter->isChain()) {
|
||||
if ($filter instanceof FilterAnd) {
|
||||
$operator = ' AND ';
|
||||
} elseif ($filter instanceof FilterOr) {
|
||||
$operator = ' OR ';
|
||||
} elseif ($filter instanceof FilterNot) {
|
||||
$operator = ' AND ';
|
||||
$where .= ' NOT ';
|
||||
} else {
|
||||
throw new ProgrammingError('Cannot render filter: %s', get_class($filter));
|
||||
}
|
||||
|
||||
if (! $filter->isEmpty()) {
|
||||
$parts = array();
|
||||
foreach ($filter->filters() as $filterPart) {
|
||||
$part = $this->renderFilter($filterPart, $level + 1);
|
||||
if ($part) {
|
||||
$parts[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($parts)) {
|
||||
if ($level > 0) {
|
||||
$where .= ' (' . implode($operator, $parts) . ') ';
|
||||
} else {
|
||||
$where .= implode($operator, $parts);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return ''; // Explicitly return the empty string due to the FilterNot case
|
||||
}
|
||||
} else {
|
||||
$where .= $this->renderFilterExpression($filter);
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render and return the given filter expression
|
||||
*
|
||||
* @param Filter $filter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderFilterExpression(Filter $filter)
|
||||
{
|
||||
$column = $filter->getColumn();
|
||||
$sign = $filter->getSign();
|
||||
$value = $filter->getExpression();
|
||||
|
||||
if (is_array($value) && $sign === '=') {
|
||||
// TODO: Should we support this? Doesn't work for blub*
|
||||
return $column . ' IN (' . $this->dbAdapter->quote($value) . ')';
|
||||
} elseif ($sign === '=' && strpos($value, '*') !== false) {
|
||||
return $column . ' LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value));
|
||||
} elseif ($sign === '!=' && strpos($value, '*') !== false) {
|
||||
return $column . ' NOT LIKE ' . $this->dbAdapter->quote(preg_replace('~\*~', '%', $value));
|
||||
} else {
|
||||
return $column . ' ' . $sign . ' ' . $this->dbAdapter->quote($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
namespace Icinga\Data\Db;
|
||||
|
||||
use Icinga\Data\SimpleQuery;
|
||||
use Icinga\Application\Benchmark;
|
||||
use Icinga\Data\Filter\FilterChain;
|
||||
use Icinga\Data\Filter\FilterExpression;
|
||||
use Icinga\Data\Filter\FilterOr;
|
||||
use Icinga\Data\Filter\FilterAnd;
|
||||
use Icinga\Data\Filter\FilterNot;
|
||||
use Icinga\Exception\IcingaException;
|
||||
use Icinga\Exception\QueryException;
|
||||
use Zend_Db_Select;
|
||||
|
||||
/**
|
||||
|
@ -68,6 +66,7 @@ class DbQuery extends SimpleQuery
|
|||
protected function init()
|
||||
{
|
||||
$this->db = $this->ds->getDbAdapter();
|
||||
$this->select = $this->db->select();
|
||||
parent::init();
|
||||
}
|
||||
|
||||
|
@ -77,6 +76,13 @@ class DbQuery extends SimpleQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function from($target, array $fields = null)
|
||||
{
|
||||
parent::from($target, $fields);
|
||||
$this->select->from($this->target, array());
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where($condition, $value = null)
|
||||
{
|
||||
// $this->count = $this->select = null;
|
||||
|
@ -85,9 +91,6 @@ class DbQuery extends SimpleQuery
|
|||
|
||||
protected function dbSelect()
|
||||
{
|
||||
if ($this->select === null) {
|
||||
$this->select = $this->db->select()->from($this->target, array());
|
||||
}
|
||||
return clone $this->select;
|
||||
}
|
||||
|
||||
|
@ -153,7 +156,7 @@ class DbQuery extends SimpleQuery
|
|||
$op = ' AND ';
|
||||
$str .= ' NOT ';
|
||||
} else {
|
||||
throw new IcingaException(
|
||||
throw new QueryException(
|
||||
'Cannot render filter: %s',
|
||||
$filter
|
||||
);
|
||||
|
@ -214,7 +217,7 @@ class DbQuery extends SimpleQuery
|
|||
if (! $value) {
|
||||
/*
|
||||
NOTE: It's too late to throw exceptions, we might finish in __toString
|
||||
throw new IcingaException(sprintf(
|
||||
throw new QueryException(sprintf(
|
||||
'"%s" is not a valid time expression',
|
||||
$value
|
||||
));
|
||||
|
@ -298,10 +301,9 @@ class DbQuery extends SimpleQuery
|
|||
public function count()
|
||||
{
|
||||
if ($this->count === null) {
|
||||
Benchmark::measure('DB is counting');
|
||||
$this->count = $this->db->fetchOne($this->getCountQuery());
|
||||
Benchmark::measure('DB finished count');
|
||||
$this->count = parent::count();
|
||||
}
|
||||
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
|
@ -321,10 +323,8 @@ class DbQuery extends SimpleQuery
|
|||
|
||||
public function __clone()
|
||||
{
|
||||
if ($this->select) {
|
||||
$this->select = clone $this->select;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
|
@ -346,4 +346,137 @@ class DbQuery extends SimpleQuery
|
|||
$this->group = $group;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the given table has been joined
|
||||
*
|
||||
* @param string $table
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasJoinedTable($table)
|
||||
{
|
||||
$fromPart = $this->select->getPart(Zend_Db_Select::FROM);
|
||||
if (isset($fromPart[$table])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($fromPart as $options) {
|
||||
if ($options['tableName'] === $table && $options['joinType'] !== Zend_Db_Select::FROM) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an INNER JOIN table and colums to the query
|
||||
*
|
||||
* @param array|string|Zend_Db_Expr $name The table name
|
||||
* @param string $cond Join on this condition
|
||||
* @param array|string $cols The columns to select from the joined table
|
||||
* @param string $schema The database name to specify, if any
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function join($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
|
||||
{
|
||||
$this->select->joinInner($name, $cond, $cols, $schema);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an INNER JOIN table and colums to the query
|
||||
*
|
||||
* @param array|string|Zend_Db_Expr $name The table name
|
||||
* @param string $cond Join on this condition
|
||||
* @param array|string $cols The columns to select from the joined table
|
||||
* @param string $schema The database name to specify, if any
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function joinInner($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
|
||||
{
|
||||
$this->select->joinInner($name, $cond, $cols, $schema);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT OUTER JOIN table and colums to the query
|
||||
*
|
||||
* @param array|string|Zend_Db_Expr $name The table name
|
||||
* @param string $cond Join on this condition
|
||||
* @param array|string $cols The columns to select from the joined table
|
||||
* @param string $schema The database name to specify, if any
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function joinLeft($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
|
||||
{
|
||||
$this->select->joinLeft($name, $cond, $cols, $schema);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RIGHT OUTER JOIN table and colums to the query
|
||||
*
|
||||
* @param array|string|Zend_Db_Expr $name The table name
|
||||
* @param string $cond Join on this condition
|
||||
* @param array|string $cols The columns to select from the joined table
|
||||
* @param string $schema The database name to specify, if any
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function joinRight($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
|
||||
{
|
||||
$this->select->joinRight($name, $cond, $cols, $schema);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a FULL OUTER JOIN table and colums to the query
|
||||
*
|
||||
* @param array|string|Zend_Db_Expr $name The table name
|
||||
* @param string $cond Join on this condition
|
||||
* @param array|string $cols The columns to select from the joined table
|
||||
* @param string $schema The database name to specify, if any
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function joinFull($name, $cond, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
|
||||
{
|
||||
$this->select->joinFull($name, $cond, $cols, $schema);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a CROSS JOIN table and colums to the query
|
||||
*
|
||||
* @param array|string|Zend_Db_Expr $name The table name
|
||||
* @param array|string $cols The columns to select from the joined table
|
||||
* @param string $schema The database name to specify, if any
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function joinCross($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
|
||||
{
|
||||
$this->select->joinCross($name, $cols, $schema);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a NATURAL JOIN table and colums to the query
|
||||
*
|
||||
* @param array|string|Zend_Db_Expr $name The table name
|
||||
* @param array|string $cols The columns to select from the joined table
|
||||
* @param string $schema The database name to specify, if any
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function joinNatural($name, $cols = Zend_Db_Select::SQL_WILDCARD, $schema = null)
|
||||
{
|
||||
$this->select->joinNatural($name, $cols, $schema);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -23,13 +23,11 @@ interface Fetchable
|
|||
public function fetchRow();
|
||||
|
||||
/**
|
||||
* Fetch a column of all rows of the result set as an array
|
||||
*
|
||||
* @param int $columnIndex Index of the column to fetch
|
||||
* Fetch the first column of all rows of the result set as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchColumn($columnIndex = 0);
|
||||
public function fetchColumn();
|
||||
|
||||
/**
|
||||
* Fetch the first column of the first row of the result set
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
namespace Icinga\Data;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Util\ConfigAwareFactory;
|
||||
use Icinga\Exception\ConfigurationError;
|
||||
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.
|
||||
*
|
||||
* @throws ProgrammingError
|
||||
* @throws ConfigurationError
|
||||
*/
|
||||
private static function assertResourcesExist()
|
||||
{
|
||||
if (!isset(self::$resources)) {
|
||||
throw new ProgrammingError(
|
||||
'The ResourceFactory must be initialised by setting a config, before it can be used'
|
||||
if (self::$resources === null) {
|
||||
throw new ConfigurationError(
|
||||
'Resources not set up. Please contact your Icinga Web administrator'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,16 @@
|
|||
|
||||
namespace Icinga\Data;
|
||||
|
||||
use ArrayIterator;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use Zend_Paginator;
|
||||
use Icinga\Application\Icinga;
|
||||
use Icinga\Application\Benchmark;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Exception\IcingaException;
|
||||
use Icinga\Web\Paginator\Adapter\QueryAdapter;
|
||||
|
||||
class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
||||
class SimpleQuery implements QueryInterface, Queryable, Iterator
|
||||
{
|
||||
/**
|
||||
* Query data source
|
||||
|
@ -21,9 +22,18 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
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
|
||||
|
@ -43,6 +53,15 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
@ -92,16 +111,6 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
*/
|
||||
protected function init() {}
|
||||
|
||||
/**
|
||||
* Return a iterable for this query's result
|
||||
*
|
||||
* @return ArrayIterator
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->fetchAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data source
|
||||
*
|
||||
|
@ -113,9 +122,73 @@ 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 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
|
||||
*/
|
||||
|
@ -226,32 +299,42 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function compare($a, $b, $col_num = 0)
|
||||
/**
|
||||
* Compare $a with $b based on this query's sort rules and column aliases
|
||||
*
|
||||
* @param object $a
|
||||
* @param object $b
|
||||
* @param int $orderIndex
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function compare($a, $b, $orderIndex = 0)
|
||||
{
|
||||
// Last column to sort reached, rows are considered being equal
|
||||
if (! array_key_exists($col_num, $this->order)) {
|
||||
return 0;
|
||||
if (! array_key_exists($orderIndex, $this->order)) {
|
||||
return 0; // Last column to sort reached, rows are considered being equal
|
||||
}
|
||||
$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);
|
||||
if ($this->flippedColumns === null) {
|
||||
$this->flippedColumns = array_flip($this->columns);
|
||||
}
|
||||
|
||||
$column = $this->order[$orderIndex][0];
|
||||
if (array_key_exists($column, $this->flippedColumns)) {
|
||||
$column = $this->flippedColumns[$column];
|
||||
}
|
||||
|
||||
// TODO: throw Exception if column is missing
|
||||
//$res = strnatcmp(strtolower($a->$column), strtolower($b->$column));
|
||||
$result = @strcmp(strtolower($a->$column), strtolower($b->$column));
|
||||
if ($result === 0) {
|
||||
return $this->compare($a, $b, ++$orderIndex);
|
||||
}
|
||||
|
||||
$direction = $this->order[$orderIndex][1];
|
||||
if ($direction === self::SORT_ASC) {
|
||||
return $result;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($dir === self::SORT_ASC) {
|
||||
return $res;
|
||||
} else {
|
||||
return $res * -1;
|
||||
return $result * -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -374,7 +457,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
*/
|
||||
public function fetchAll()
|
||||
{
|
||||
return $this->ds->fetchAll($this);
|
||||
Benchmark::measure('Fetching all results started');
|
||||
$results = $this->ds->fetchAll($this);
|
||||
Benchmark::measure('Fetching all results finished');
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -384,19 +470,23 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
*/
|
||||
public function fetchRow()
|
||||
{
|
||||
return $this->ds->fetchRow($this);
|
||||
Benchmark::measure('Fetching one row started');
|
||||
$row = $this->ds->fetchRow($this);
|
||||
Benchmark::measure('Fetching one row finished');
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a column of all rows of the result set as an array
|
||||
*
|
||||
* @param int $columnIndex Index of the column to fetch
|
||||
* Fetch the first column of all rows of the result set as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchColumn($columnIndex = 0)
|
||||
public function fetchColumn()
|
||||
{
|
||||
return $this->ds->fetchColumn($this, $columnIndex);
|
||||
Benchmark::measure('Fetching one column started');
|
||||
$values = $this->ds->fetchColumn($this);
|
||||
Benchmark::measure('Fetching one column finished');
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -406,7 +496,10 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
*/
|
||||
public function fetchOne()
|
||||
{
|
||||
return $this->ds->fetchOne($this);
|
||||
Benchmark::measure('Fetching one value started');
|
||||
$value = $this->ds->fetchOne($this);
|
||||
Benchmark::measure('Fetching one value finished');
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -418,17 +511,25 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
*/
|
||||
public function fetchPairs()
|
||||
{
|
||||
return $this->ds->fetchPairs($this);
|
||||
Benchmark::measure('Fetching pairs started');
|
||||
$pairs = $this->ds->fetchPairs($this);
|
||||
Benchmark::measure('Fetching pairs finished');
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all rows of the result set
|
||||
* Count all rows of the result set, ignoring limit and offset
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return $this->ds->count($this);
|
||||
$query = clone $this;
|
||||
$query->limit(0, 0);
|
||||
Benchmark::measure('Counting all results started');
|
||||
$count = $this->ds->count($query);
|
||||
Benchmark::measure('Counting all results finished');
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -441,6 +542,7 @@ class SimpleQuery implements QueryInterface, Queryable, IteratorAggregate
|
|||
public function columns(array $columns)
|
||||
{
|
||||
$this->columns = $columns;
|
||||
$this->flippedColumns = null; // Reset, due to updated columns
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Exception;
|
||||
|
||||
class StatementException extends IcingaException
|
||||
{
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
namespace Icinga\Protocol\File;
|
||||
|
||||
use Countable;
|
||||
use ArrayIterator;
|
||||
use Icinga\Data\Selectable;
|
||||
use Icinga\Data\ConfigObject;
|
||||
|
||||
|
@ -71,6 +72,18 @@ class FileReader implements Selectable, Countable
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
|
||||
namespace Icinga\Protocol\Ldap;
|
||||
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Protocol\Ldap\Exception as LdapException;
|
||||
use Icinga\Application\Platform;
|
||||
use ArrayIterator;
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Application\Platform;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Data\Selectable;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\Protocol\Ldap\Exception as LdapException;
|
||||
|
||||
/**
|
||||
* Backend class managing all the LDAP stuff for you.
|
||||
|
@ -24,7 +26,7 @@ use Icinga\Data\ConfigObject;
|
|||
* ));
|
||||
* </code>
|
||||
*/
|
||||
class Connection
|
||||
class Connection implements Selectable
|
||||
{
|
||||
const LDAP_NO_SUCH_OBJECT = 32;
|
||||
const LDAP_SIZELIMIT_EXCEEDED = 4;
|
||||
|
@ -122,11 +124,21 @@ class Connection
|
|||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a query on this connection
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function select()
|
||||
{
|
||||
return new Query($this);
|
||||
}
|
||||
|
||||
public function query(Query $query)
|
||||
{
|
||||
return new ArrayIterator($this->fetchAll($query));
|
||||
}
|
||||
|
||||
public function fetchOne($query, $fields = array())
|
||||
{
|
||||
$row = (array) $this->fetchRow($query, $fields);
|
||||
|
@ -198,13 +210,13 @@ class Connection
|
|||
* @return string Returns the distinguished name, or false when the given query yields no results
|
||||
* @throws LdapException When the query result is empty and contains no DN to fetch
|
||||
*/
|
||||
public function fetchDN(Query $query, $fields = array())
|
||||
public function fetchDn(Query $query, $fields = array())
|
||||
{
|
||||
$rows = $this->fetchAll($query, $fields);
|
||||
if (count($rows) !== 1) {
|
||||
if (count($rows) > 1) {
|
||||
throw new LdapException(
|
||||
'Cannot fetch single DN for %s',
|
||||
$query->create()
|
||||
$query
|
||||
);
|
||||
}
|
||||
return key($rows);
|
||||
|
@ -235,14 +247,9 @@ class Connection
|
|||
$this->connect();
|
||||
$this->bind();
|
||||
|
||||
$count = 0;
|
||||
$results = $this->runQuery($query);
|
||||
while (! empty($results)) {
|
||||
$count += ldap_count_entries($this->ds, $results);
|
||||
$results = $this->runQuery($query);
|
||||
}
|
||||
|
||||
return $count;
|
||||
// TODO: That's still not the best solution, this should probably not request any attributes
|
||||
$res = $this->runQuery($query);
|
||||
return count($res);
|
||||
}
|
||||
|
||||
public function fetchAll(Query $query, $fields = array())
|
||||
|
@ -266,16 +273,20 @@ class Connection
|
|||
* @return array The matched entries
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function runQuery(Query $query, $fields = array())
|
||||
protected function runQuery(Query $query, array $fields = null)
|
||||
{
|
||||
$limit = $query->getLimit();
|
||||
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
|
||||
|
||||
if (empty($fields)) {
|
||||
$fields = $query->getColumns();
|
||||
}
|
||||
|
||||
$results = @ldap_search(
|
||||
$this->ds,
|
||||
$query->hasBase() ? $query->getBase() : $this->root_dn,
|
||||
$query->create(),
|
||||
empty($fields) ? $query->listFields() : $fields,
|
||||
$query->getBase() ?: $this->root_dn,
|
||||
(string) $query,
|
||||
array_values($fields),
|
||||
0, // Attributes and values
|
||||
$limit ? $offset + $limit : 0
|
||||
);
|
||||
|
@ -286,18 +297,14 @@ class Connection
|
|||
|
||||
throw new LdapException(
|
||||
'LDAP query "%s" (base %s) failed. Error: %s',
|
||||
$query->create(),
|
||||
$query->hasBase() ? $query->getBase() : $this->root_dn,
|
||||
$query,
|
||||
$query->getBase() ?: $this->root_dn,
|
||||
ldap_error($this->ds)
|
||||
);
|
||||
} elseif (ldap_count_entries($this->ds, $results) === 0) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ($query->getSortColumns() as $col) {
|
||||
ldap_sort($this->ds, $results, $col[0]);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$entries = array();
|
||||
$entry = ldap_first_entry($this->ds, $results);
|
||||
|
@ -305,11 +312,15 @@ class Connection
|
|||
$count += 1;
|
||||
if ($offset === 0 || $offset < $count) {
|
||||
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes(
|
||||
ldap_get_attributes($this->ds, $entry)
|
||||
ldap_get_attributes($this->ds, $entry), array_flip($fields)
|
||||
);
|
||||
}
|
||||
} while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry)));
|
||||
|
||||
if ($query->hasOrder()) {
|
||||
uasort($entries, array($query, 'compare'));
|
||||
}
|
||||
|
||||
ldap_free_result($results);
|
||||
return $entries;
|
||||
}
|
||||
|
@ -330,28 +341,29 @@ class Connection
|
|||
*
|
||||
* @param Query $query The query to execute
|
||||
* @param array $fields The fields that will be fetched from the matches
|
||||
* @param int $page_size The maximum page size, defaults to Connection::PAGE_SIZE
|
||||
* @param int $pageSize The maximum page size, defaults to Connection::PAGE_SIZE
|
||||
*
|
||||
* @return array The matched entries
|
||||
* @throws LdapException
|
||||
* @throws ProgrammingError When executed without available page controls (check with pageControlAvailable() )
|
||||
*/
|
||||
protected function runPagedQuery(Query $query, $fields = array(), $pageSize = null)
|
||||
protected function runPagedQuery(Query $query, array $fields = null, $pageSize = null)
|
||||
{
|
||||
if (! $this->pageControlAvailable($query)) {
|
||||
throw new ProgrammingError('LDAP: Page control not available.');
|
||||
}
|
||||
|
||||
if (! isset($pageSize)) {
|
||||
$pageSize = static::PAGE_SIZE;
|
||||
}
|
||||
|
||||
$limit = $query->getLimit();
|
||||
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
|
||||
$queryString = $query->create();
|
||||
$base = $query->hasBase() ? $query->getBase() : $this->root_dn;
|
||||
$queryString = (string) $query;
|
||||
$base = $query->getBase() ?: $this->root_dn;
|
||||
|
||||
if (empty($fields)) {
|
||||
$fields = $query->listFields();
|
||||
$fields = $query->getColumns();
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
@ -362,7 +374,14 @@ class Connection
|
|||
// possibillity server to return an answer in case the pagination extension is missing.
|
||||
ldap_control_paged_result($this->ds, $pageSize, false, $cookie);
|
||||
|
||||
$results = @ldap_search($this->ds, $base, $queryString, $fields, 0, $limit ? $offset + $limit : 0);
|
||||
$results = @ldap_search(
|
||||
$this->ds,
|
||||
$base,
|
||||
$queryString,
|
||||
array_values($fields),
|
||||
0, // Attributes and values
|
||||
$limit ? $offset + $limit : 0
|
||||
);
|
||||
if ($results === false) {
|
||||
if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
|
||||
break;
|
||||
|
@ -394,7 +413,7 @@ class Connection
|
|||
$count += 1;
|
||||
if ($offset === 0 || $offset < $count) {
|
||||
$entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes(
|
||||
ldap_get_attributes($this->ds, $entry)
|
||||
ldap_get_attributes($this->ds, $entry), array_flip($fields)
|
||||
);
|
||||
}
|
||||
} while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry)));
|
||||
|
@ -420,29 +439,61 @@ class Connection
|
|||
// pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by
|
||||
// the server: https://www.ietf.org/rfc/rfc2696.txt
|
||||
ldap_control_paged_result($this->ds, 0, false, $cookie);
|
||||
ldap_search($this->ds, $base, $queryString, $fields); // Returns no entries, due to the page size
|
||||
ldap_search($this->ds, $base, $queryString); // Returns no entries, due to the page size
|
||||
} else {
|
||||
// Reset the paged search request so that subsequent requests succeed
|
||||
ldap_control_paged_result($this->ds, 0);
|
||||
}
|
||||
|
||||
if ($query->hasOrder()) {
|
||||
uasort($entries, array($query, 'compare'));
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
protected function cleanupAttributes($attrs)
|
||||
protected function cleanupAttributes($attributes, array $requestedFields)
|
||||
{
|
||||
$clean = (object) array();
|
||||
for ($i = 0; $i < $attrs['count']; $i++) {
|
||||
$attr_name = $attrs[$i];
|
||||
if ($attrs[$attr_name]['count'] === 1) {
|
||||
$clean->$attr_name = $attrs[$attr_name][0];
|
||||
// In case the result contains attributes with a differing case than the requested fields, it is
|
||||
// necessary to create another array to map attributes case insensitively to their requested counterparts.
|
||||
// This does also apply the virtual alias handling. (Since an LDAP server does not handle such)
|
||||
$loweredFieldMap = array();
|
||||
foreach ($requestedFields as $name => $alias) {
|
||||
$loweredFieldMap[strtolower($name)] = is_string($alias) ? $alias : $name;
|
||||
}
|
||||
|
||||
$cleanedAttributes = array();
|
||||
for ($i = 0; $i < $attributes['count']; $i++) {
|
||||
$attribute_name = $attributes[$i];
|
||||
if ($attributes[$attribute_name]['count'] === 1) {
|
||||
$attribute_value = $attributes[$attribute_name][0];
|
||||
} else {
|
||||
for ($j = 0; $j < $attrs[$attr_name]['count']; $j++) {
|
||||
$clean->{$attr_name}[] = $attrs[$attr_name][$j];
|
||||
$attribute_value = array();
|
||||
for ($j = 0; $j < $attributes[$attribute_name]['count']; $j++) {
|
||||
$attribute_value[] = $attributes[$attribute_name][$j];
|
||||
}
|
||||
}
|
||||
|
||||
$requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)])
|
||||
? $loweredFieldMap[strtolower($attribute_name)]
|
||||
: $attribute_name;
|
||||
$cleanedAttributes[$requestedAttributeName] = $attribute_value;
|
||||
}
|
||||
return $clean;
|
||||
|
||||
// The result may not contain all requested fields, so populate the cleaned
|
||||
// result with the missing fields and their value being set to null
|
||||
foreach ($requestedFields as $name => $alias) {
|
||||
if (! is_string($alias)) {
|
||||
$alias = $name;
|
||||
}
|
||||
|
||||
if (! array_key_exists($alias, $cleanedAttributes)) {
|
||||
$cleanedAttributes[$alias] = null;
|
||||
Logger::debug('LDAP query result does not provide the requested field "%s"', $name);
|
||||
}
|
||||
}
|
||||
|
||||
return (object) $cleanedAttributes;
|
||||
}
|
||||
|
||||
public function testCredentials($username, $password)
|
||||
|
@ -566,6 +617,10 @@ class Connection
|
|||
*/
|
||||
public function getCapabilities()
|
||||
{
|
||||
if ($this->capabilities === null) {
|
||||
$this->connect(); // Populates $this->capabilities
|
||||
}
|
||||
|
||||
return $this->capabilities;
|
||||
}
|
||||
|
||||
|
@ -590,9 +645,7 @@ class Connection
|
|||
*/
|
||||
protected function discoverCapabilities($ds)
|
||||
{
|
||||
$query = $this->select()->from(
|
||||
'*',
|
||||
array(
|
||||
$fields = array(
|
||||
'defaultNamingContext',
|
||||
'namingContexts',
|
||||
'vendorName',
|
||||
|
@ -605,15 +658,9 @@ class Connection
|
|||
'supportedControl',
|
||||
'supportedExtension',
|
||||
'+'
|
||||
)
|
||||
);
|
||||
$result = @ldap_read(
|
||||
$ds,
|
||||
'',
|
||||
$query->create(),
|
||||
$query->listFields()
|
||||
);
|
||||
|
||||
$result = @ldap_read($ds, '', (string) $this->select()->from('*', $fields), $fields);
|
||||
if (! $result) {
|
||||
throw new LdapException(
|
||||
'Capability query failed (%s:%d): %s. Check if hostname and port of the'
|
||||
|
@ -623,6 +670,7 @@ class Connection
|
|||
ldap_error($ds)
|
||||
);
|
||||
}
|
||||
|
||||
$entry = ldap_first_entry($ds, $result);
|
||||
if ($entry === false) {
|
||||
throw new LdapException(
|
||||
|
@ -633,9 +681,7 @@ class Connection
|
|||
);
|
||||
}
|
||||
|
||||
$ldapAttributes = ldap_get_attributes($ds, $entry);
|
||||
$result = $this->cleanupAttributes($ldapAttributes);
|
||||
return new Capability($result);
|
||||
return new Capability($this->cleanupAttributes(ldap_get_attributes($ds, $entry), array_flip($fields)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,151 +3,154 @@
|
|||
|
||||
namespace Icinga\Protocol\Ldap;
|
||||
|
||||
use Icinga\Data\SimpleQuery;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Exception\NotImplementedError;
|
||||
|
||||
/**
|
||||
* Search class
|
||||
*
|
||||
* @package Icinga\Protocol\Ldap
|
||||
* LDAP query class
|
||||
*/
|
||||
/**
|
||||
* Search abstraction class
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* <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
|
||||
class Query extends SimpleQuery
|
||||
{
|
||||
protected $connection;
|
||||
protected $filters = array();
|
||||
protected $fields = array();
|
||||
protected $limit_count = 0;
|
||||
protected $limit_offset = 0;
|
||||
protected $sort_columns = array();
|
||||
protected $count;
|
||||
protected $base;
|
||||
protected $usePagedResults = true;
|
||||
/**
|
||||
* This query's filters
|
||||
*
|
||||
* Currently just a basic key/value pair based array. Can be removed once Icinga\Data\Filter is supported.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* The base dn being used for this query
|
||||
*
|
||||
* @param Connection LDAP Connection object
|
||||
* @return void
|
||||
* @var string
|
||||
*/
|
||||
public function __construct(Connection $connection)
|
||||
protected $base;
|
||||
|
||||
/**
|
||||
* Whether this query is permitted to utilize paged results
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $usePagedResults;
|
||||
|
||||
/**
|
||||
* Initialize this query
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->filters = array();
|
||||
$this->usePagedResults = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the base dn to be used for this query
|
||||
*
|
||||
* @param string $base
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBase($base)
|
||||
{
|
||||
$this->base = $base;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasBase()
|
||||
{
|
||||
return $this->base !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base dn being used for this query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBase()
|
||||
{
|
||||
return $this->base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this query is permitted to utilize paged results
|
||||
*
|
||||
* @param bool $state
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setUsePagedResults($state = true)
|
||||
{
|
||||
$this->usePagedResults = (bool) $state;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether this query is permitted to utilize paged results
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUsePagedResults()
|
||||
{
|
||||
return $this->usePagedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count result set, ignoring limits
|
||||
* Choose an objectClass and the columns you are interested in
|
||||
*
|
||||
* @return int
|
||||
* {@inheritdoc} This creates an objectClass filter.
|
||||
*/
|
||||
public function count()
|
||||
public function from($target, array $fields = null)
|
||||
{
|
||||
if ($this->count === null) {
|
||||
$this->count = $this->connection->count($this);
|
||||
}
|
||||
return $this->count;
|
||||
$this->filters['objectClass'] = $target;
|
||||
return parent::from($target, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count result set, ignoring limits
|
||||
* Add a new filter to the query
|
||||
*
|
||||
* @return int
|
||||
* @param string $condition Column to search in
|
||||
* @param mixed $value Value to look for (asterisk wildcards are allowed)
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function limit($count = null, $offset = null)
|
||||
public function where($condition, $value = null)
|
||||
{
|
||||
if (! preg_match('~^\d+~', $count . $offset)) {
|
||||
throw new Exception(
|
||||
'Got invalid limit: %s, %s',
|
||||
$count,
|
||||
$offset
|
||||
);
|
||||
// TODO: Adjust this once support for Icinga\Data\Filter is available
|
||||
if ($condition instanceof Expression) {
|
||||
$this->filters[] = $condition;
|
||||
} else {
|
||||
$this->filters[$condition] = $value;
|
||||
}
|
||||
$this->limit_count = (int) $count;
|
||||
$this->limit_offset = (int) $offset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a limit has been set
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasLimit()
|
||||
public function getFilter()
|
||||
{
|
||||
return $this->limit_count > 0;
|
||||
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an offset (limit) has been set
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasOffset()
|
||||
public function addFilter(Filter $filter)
|
||||
{
|
||||
return $this->limit_offset > 0;
|
||||
// TODO: This should be considered a quick fix only.
|
||||
// Drop this entirely once support for Icinga\Data\Filter is available
|
||||
if ($filter->isExpression()) {
|
||||
$this->where($filter->getColumn(), $filter->getExpression());
|
||||
} elseif ($filter->isChain()) {
|
||||
foreach ($filter->filters() as $chainOrExpression) {
|
||||
$this->addFilter($chainOrExpression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve result limit
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLimit()
|
||||
public function setFilter(Filter $filter)
|
||||
{
|
||||
return $this->limit_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve result offset
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOffset()
|
||||
{
|
||||
return $this->limit_offset;
|
||||
throw new NotImplementedError('Support for Icinga\Data\Filter is still missing. Use $this->where() instead');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch result as tree
|
||||
*
|
||||
* @return Node
|
||||
* @return Root
|
||||
*
|
||||
* @todo This is untested waste, not being used anywhere and ignores the query's order and base dn.
|
||||
* Evaluate whether it's reasonable to properly implement and test it.
|
||||
*/
|
||||
public function fetchTree()
|
||||
{
|
||||
|
@ -179,132 +182,32 @@ class Query
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch result as an array of objects
|
||||
* Fetch the distinguished name of the first result
|
||||
*
|
||||
* @return array
|
||||
* @return string|false The distinguished name or false in case it's not possible to fetch a result
|
||||
*
|
||||
* @throws Exception In case the query returns multiple results
|
||||
* (i.e. it's not possible to fetch a unique DN)
|
||||
*/
|
||||
public function fetchAll()
|
||||
public function fetchDn()
|
||||
{
|
||||
return $this->connection->fetchAll($this);
|
||||
return $this->ds->fetchDn($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch first result row
|
||||
* Return the LDAP filter to be applied on this query
|
||||
*
|
||||
* @return object
|
||||
* @return string
|
||||
*
|
||||
* @throws Exception In case the objectClass filter does not exist
|
||||
*/
|
||||
public function fetchRow()
|
||||
protected function renderFilter()
|
||||
{
|
||||
return $this->connection->fetchRow($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch first column value from first result row
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function fetchOne()
|
||||
{
|
||||
return $this->connection->fetchOne($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a key/value list, first column is key, second is value
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchPairs()
|
||||
{
|
||||
// STILL TODO!!
|
||||
return $this->connection->fetchPairs($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Where to select (which fields) from
|
||||
*
|
||||
* This creates an objectClass filter
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function from($objectClass, $fields = array())
|
||||
{
|
||||
$this->filters['objectClass'] = $objectClass;
|
||||
$this->fields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new filter to the query
|
||||
*
|
||||
* @param string Column to search in
|
||||
* @param string Filter text (asterisks are allowed)
|
||||
* @return Query
|
||||
*/
|
||||
public function where($key, $val)
|
||||
{
|
||||
$this->filters[$key] = $val;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by given column
|
||||
*
|
||||
* TODO: Sort direction is not implemented yet
|
||||
*
|
||||
* @param string Order column
|
||||
* @param string Order direction
|
||||
* @return Query
|
||||
*/
|
||||
public function order($column, $direction = 'ASC')
|
||||
{
|
||||
$this->sort_columns[] = array($column, $direction);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of the desired fields
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function listFields()
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list containing current sort columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSortColumns()
|
||||
{
|
||||
return $this->sort_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filter expression to this query
|
||||
*
|
||||
* @param Expression $expression
|
||||
*
|
||||
* @return Query
|
||||
*/
|
||||
public function addFilter(Expression $expression)
|
||||
{
|
||||
$this->filters[] = $expression;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the LDAP filter that will be applied
|
||||
*
|
||||
* @string
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$parts = array();
|
||||
if (! isset($this->filters['objectClass']) || $this->filters['objectClass'] === null) {
|
||||
if (! isset($this->filters['objectClass'])) {
|
||||
throw new Exception('Object class is mandatory');
|
||||
}
|
||||
|
||||
$parts = array();
|
||||
foreach ($this->filters as $key => $value) {
|
||||
if ($value instanceof Expression) {
|
||||
$parts[] = (string) $value;
|
||||
|
@ -316,6 +219,7 @@ class Query
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($parts) > 1) {
|
||||
return '(&(' . implode(')(', $parts) . '))';
|
||||
} else {
|
||||
|
@ -323,17 +227,13 @@ class Query
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the LDAP filter to be applied on this query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Descructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
// To be on the safe side:
|
||||
unset($this->connection);
|
||||
return $this->renderFilter();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -426,7 +426,7 @@ class User
|
|||
// matches
|
||||
$any = strpos($requiredPermission, '*');
|
||||
foreach ($this->permissions as $grantedPermission) {
|
||||
if ($any !== false && strpos($grantedPermission, '*') === false) {
|
||||
if ($any !== false) {
|
||||
$wildcard = $any;
|
||||
} else {
|
||||
// If the permit contains a wildcard, grant the permission if it's related to the permit
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -39,7 +39,7 @@ class ErrorLabeller extends Zend_Translate_Adapter
|
|||
|
||||
protected function createMessages($element)
|
||||
{
|
||||
$label = $element->getLabel();
|
||||
$label = $element->getLabel() ?: $element->getName();
|
||||
|
||||
return array(
|
||||
Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
|
||||
|
|
|
@ -233,40 +233,50 @@ class Menu implements RecursiveIterator
|
|||
));
|
||||
|
||||
$section = $this->add(t('System'), array(
|
||||
'icon' => 'wrench',
|
||||
'priority' => 200,
|
||||
'icon' => 'services',
|
||||
'priority' => 700,
|
||||
'renderer' => 'ProblemMenuItemRenderer'
|
||||
));
|
||||
$section->add(t('Configuration'), array(
|
||||
if (Logger::writesToFile()) {
|
||||
$section->add(t('Application Log'), array(
|
||||
'url' => 'list/applicationlog',
|
||||
'priority' => 710
|
||||
));
|
||||
}
|
||||
|
||||
$section = $this->add(t('Configuration'), array(
|
||||
'icon' => 'wrench',
|
||||
'permission' => 'config/*',
|
||||
'priority' => 800
|
||||
));
|
||||
$section->add(t('Application'), array(
|
||||
'url' => 'config',
|
||||
'permission' => 'config/application/*',
|
||||
'priority' => 300
|
||||
'priority' => 810
|
||||
));
|
||||
$section->add(t('Authentication'), array(
|
||||
'url' => 'user',
|
||||
'permission' => 'config/authentication/*',
|
||||
'priority' => 820
|
||||
));
|
||||
$section->add(t('Modules'), array(
|
||||
'url' => 'config/modules',
|
||||
'permission' => 'config/modules',
|
||||
'priority' => 400
|
||||
'priority' => 890
|
||||
));
|
||||
|
||||
if (Logger::writesToFile()) {
|
||||
$section->add(t('Application Log'), array(
|
||||
'url' => 'list/applicationlog',
|
||||
'priority' => 500
|
||||
));
|
||||
}
|
||||
|
||||
$section = $this->add($auth->getUser()->getUsername(), array(
|
||||
'icon' => 'user',
|
||||
'priority' => 600
|
||||
'priority' => 900
|
||||
));
|
||||
$section->add(t('Preferences'), array(
|
||||
'url' => 'preference',
|
||||
'priority' => 601
|
||||
'priority' => 910
|
||||
));
|
||||
|
||||
$section->add(t('Logout'), array(
|
||||
'url' => 'authentication/logout',
|
||||
'priority' => 700,
|
||||
'priority' => 990,
|
||||
'renderer' => 'ForeignMenuItemRenderer'
|
||||
));
|
||||
}
|
||||
|
|
|
@ -4,43 +4,63 @@
|
|||
namespace Icinga\Web\Paginator\Adapter;
|
||||
|
||||
use Zend_Paginator_Adapter_Interface;
|
||||
|
||||
/**
|
||||
* @see Zend_Paginator_Adapter_Interface
|
||||
*/
|
||||
use Icinga\Data\QueryInterface;
|
||||
|
||||
class QueryAdapter implements Zend_Paginator_Adapter_Interface
|
||||
{
|
||||
/**
|
||||
* Array
|
||||
* The query being paginated
|
||||
*
|
||||
* @var array
|
||||
* @var QueryInterface
|
||||
*/
|
||||
protected $query = null;
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* Item count
|
||||
*
|
||||
* @var integer
|
||||
* @var int
|
||||
*/
|
||||
protected $count = null;
|
||||
protected $count;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Create a new QueryAdapter
|
||||
*
|
||||
* @param array $query Query to paginate
|
||||
* @param QueryInterface $query The query to paginate
|
||||
*/
|
||||
// TODO: This might be ready for (QueryInterface $query)
|
||||
public function __construct($query)
|
||||
public function __construct(QueryInterface $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->setQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of items for a page.
|
||||
* Set the query to paginate
|
||||
*
|
||||
* @param QueryInterface $query
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setQuery(QueryInterface $query)
|
||||
{
|
||||
$this->query = $query;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query being paginated
|
||||
*
|
||||
* @return QueryInterface
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and return the rows in the given range of the query result
|
||||
*
|
||||
* @param int $offset Page offset
|
||||
* @param int $itemCountPerPage Number of items per page
|
||||
*
|
||||
* @param integer $offset Page offset
|
||||
* @param integer $itemCountPerPage Number of items per page
|
||||
* @return array
|
||||
*/
|
||||
public function getItems($offset, $itemCountPerPage)
|
||||
|
@ -49,15 +69,16 @@ class QueryAdapter implements Zend_Paginator_Adapter_Interface
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of items in the query result.
|
||||
* Return the total number of items in the query result
|
||||
*
|
||||
* @return integer
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
if ($this->count === null) {
|
||||
$this->count = $this->query->count();
|
||||
}
|
||||
|
||||
return $this->count;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ $section = $this->menuSection($this->translate('Documentation'), array(
|
|||
'title' => 'Documentation',
|
||||
'icon' => 'book',
|
||||
'url' => 'doc',
|
||||
'priority' => 190
|
||||
'priority' => 700
|
||||
));
|
||||
|
||||
$section->add('Icinga Web 2', array(
|
||||
|
@ -18,7 +18,7 @@ $section->add('Module documentations', array(
|
|||
));
|
||||
$section->add($this->translate('Developer - Style'), array(
|
||||
'url' => 'doc/style/guide',
|
||||
'priority' => 200,
|
||||
'priority' => 790
|
||||
));
|
||||
|
||||
$this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10);
|
||||
|
|
|
@ -208,7 +208,7 @@ $section->add($this->translate('Alert Summary'), array(
|
|||
$section = $this->menuSection($this->translate('System'));
|
||||
$section->add($this->translate('Monitoring Health'), array(
|
||||
'url' => 'monitoring/process/info',
|
||||
'priority' => 120,
|
||||
'priority' => 720,
|
||||
'renderer' => 'Icinga\Module\Monitoring\Web\Menu\BackendAvailabilityMenuItemRenderer'
|
||||
));
|
||||
|
||||
|
|
|
@ -415,19 +415,9 @@ abstract class IdoQuery extends DbQuery
|
|||
} elseif ($dbType === 'pgsql') {
|
||||
$this->initializeForPostgres();
|
||||
}
|
||||
$this->dbSelect();
|
||||
$this->select->columns($this->columns);
|
||||
//$this->joinBaseTables();
|
||||
$this->prepareAliasIndexes();
|
||||
}
|
||||
|
||||
protected function dbSelect()
|
||||
{
|
||||
if ($this->select === null) {
|
||||
$this->select = $this->db->select();
|
||||
$this->joinBaseTables();
|
||||
}
|
||||
return clone $this->select;
|
||||
$this->select->columns($this->columns);
|
||||
$this->prepareAliasIndexes();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -108,7 +108,8 @@ abstract class CommandTransport
|
|||
*/
|
||||
public static function first()
|
||||
{
|
||||
$config = self::getConfig()->current();
|
||||
return self::fromConfig($config);
|
||||
$config = self::getConfig();
|
||||
$config->rewind();
|
||||
return self::fromConfig($config->current());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
namespace Icinga\Module\Monitoring\DataView;
|
||||
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
use Icinga\Data\QueryInterface;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
|
@ -13,6 +12,7 @@ use Icinga\Data\ConnectionInterface;
|
|||
use Icinga\Exception\QueryException;
|
||||
use Icinga\Web\Request;
|
||||
use Icinga\Web\Url;
|
||||
use Icinga\Module\Monitoring\Backend\Ido\Query\IdoQuery;
|
||||
use Icinga\Module\Monitoring\Backend\MonitoringBackend;
|
||||
|
||||
/**
|
||||
|
@ -23,7 +23,7 @@ abstract class DataView implements QueryInterface, IteratorAggregate
|
|||
/**
|
||||
* The query used to populate the view
|
||||
*
|
||||
* @var QueryInterface
|
||||
* @var IdoQuery
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
|
@ -61,11 +61,11 @@ abstract class DataView implements QueryInterface, IteratorAggregate
|
|||
/**
|
||||
* Return a iterator for all rows of the result set
|
||||
*
|
||||
* @return ArrayIterator
|
||||
* @return IdoQuery
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
return new ArrayIterator($this->fetchAll());
|
||||
return $this->getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -481,15 +481,13 @@ abstract class DataView implements QueryInterface, IteratorAggregate
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch a column of all rows of the result set as an array
|
||||
*
|
||||
* @param int $columnIndex Index of the column to fetch
|
||||
* Fetch the first column of all rows of the result set as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fetchColumn($columnIndex = 0)
|
||||
public function fetchColumn()
|
||||
{
|
||||
return $this->getQuery()->fetchColumn($columnIndex);
|
||||
return $this->getQuery()->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,8 +8,8 @@ use LogicException;
|
|||
use Icinga\Web\Form;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
use Icinga\Authentication\Backend\DbUserBackend;
|
||||
use Icinga\Authentication\Backend\LdapUserBackend;
|
||||
use Icinga\Authentication\User\DbUserBackend;
|
||||
use Icinga\Authentication\User\LdapUserBackend;
|
||||
|
||||
/**
|
||||
* Wizard page to define the initial administrative account
|
||||
|
@ -268,13 +268,8 @@ class AdminAccountPage extends Form
|
|||
if ($this->backendConfig['backend'] === 'db') {
|
||||
$backend = new DbUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
|
||||
} elseif ($this->backendConfig['backend'] === 'ldap') {
|
||||
$backend = new LdapUserBackend(
|
||||
ResourceFactory::createResource(new ConfigObject($this->resourceConfig)),
|
||||
$this->backendConfig['user_class'],
|
||||
$this->backendConfig['user_name_attribute'],
|
||||
$this->backendConfig['base_dn'],
|
||||
$this->backendConfig['filter']
|
||||
);
|
||||
$backend = new LdapUserBackend(ResourceFactory::createResource(new ConfigObject($this->resourceConfig)));
|
||||
$backend->setConfig($this->backendConfig);
|
||||
} else {
|
||||
throw new LogicException(
|
||||
sprintf(
|
||||
|
@ -285,10 +280,8 @@ class AdminAccountPage extends Form
|
|||
}
|
||||
|
||||
try {
|
||||
$users = $backend->listUsers();
|
||||
natsort ($users);
|
||||
return $users;
|
||||
} catch (Exception $e) {
|
||||
return $backend->select(array('user_name'))->fetchColumn();
|
||||
} catch (Exception $_) {
|
||||
// No need to handle anything special here. Error means no users found.
|
||||
return array();
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
namespace Icinga\Module\Setup\Forms;
|
||||
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Forms\Config\Authentication\DbBackendForm;
|
||||
use Icinga\Forms\Config\Authentication\LdapBackendForm;
|
||||
use Icinga\Forms\Config\Authentication\ExternalBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\DbBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\LdapBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
|
||||
use Icinga\Data\ConfigObject;
|
||||
|
||||
/**
|
||||
|
@ -105,7 +105,7 @@ class AuthBackendPage extends Form
|
|||
}
|
||||
|
||||
if (false === isset($data['skip_validation']) || $data['skip_validation'] == 0) {
|
||||
if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidAuthenticationBackend($this)) {
|
||||
if ($this->config['type'] === 'ldap' && false === LdapBackendForm::isValidUserBackend($this)) {
|
||||
$this->addSkipValidationCheckbox();
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use Exception;
|
|||
use Icinga\Application\Config;
|
||||
use Icinga\Data\ConfigObject;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
use Icinga\Authentication\Backend\DbUserBackend;
|
||||
use Icinga\Authentication\User\DbUserBackend;
|
||||
use Icinga\Module\Setup\Step;
|
||||
|
||||
class AuthenticationStep extends Step
|
||||
|
@ -88,11 +88,12 @@ class AuthenticationStep extends Step
|
|||
ResourceFactory::createResource(new ConfigObject($this->data['adminAccountData']['resourceConfig']))
|
||||
);
|
||||
|
||||
if (array_search($this->data['adminAccountData']['username'], $backend->listUsers()) === false) {
|
||||
$backend->addUser(
|
||||
$this->data['adminAccountData']['username'],
|
||||
$this->data['adminAccountData']['password']
|
||||
);
|
||||
if ($backend->select()->where('user_name', $this->data['adminAccountData']['username'])->count() === 0) {
|
||||
$backend->insert('user', array(
|
||||
'user_name' => $this->data['adminAccountData']['username'],
|
||||
'password' => $this->data['adminAccountData']['password'],
|
||||
'is_active' => true
|
||||
));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->dbError = $e;
|
||||
|
|
|
@ -41,6 +41,10 @@ img.icon {
|
|||
background-position: 1em center;
|
||||
}
|
||||
|
||||
#notifications > li.info {
|
||||
background-color: @colorFormNotificationInfo;
|
||||
}
|
||||
|
||||
#notifications > li.warning {
|
||||
background-color: @colorWarningHandled;
|
||||
}
|
||||
|
@ -202,3 +206,164 @@ table.benchmark {
|
|||
border: 1px solid lightgrey;
|
||||
background-color: #fbfcc5;
|
||||
}
|
||||
|
||||
div.content.users {
|
||||
table.user-list {
|
||||
th.user-remove {
|
||||
width: 8em;
|
||||
padding-right: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.user-remove {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a.user-add {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
div.controls div.user-header {
|
||||
border-bottom: 2px solid @colorPetrol;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.user-name {
|
||||
display: inline-block;
|
||||
margin: 0 0 0.3em;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.user-state, .user-created, .user-modified {
|
||||
margin: 0 0 0.2em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
div.content.memberships {
|
||||
table.membership-list {
|
||||
th.membership-cancel {
|
||||
width: 8em;
|
||||
padding-right: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.membership-cancel {
|
||||
text-align: right;
|
||||
|
||||
form button.link-like {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a.membership-create {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
div.content.groups {
|
||||
table.group-list {
|
||||
th.group-remove {
|
||||
width: 8em;
|
||||
padding-right: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.group-remove {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a.group-add {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
div.controls div.group-header {
|
||||
border-bottom: 2px solid @colorPetrol;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.group-name {
|
||||
display: inline-block;
|
||||
margin: 0 0 0.3em;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.group-parent, .group-created, .group-modified {
|
||||
margin: 0 0 0.2em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
div.content.members {
|
||||
table.member-list {
|
||||
th.member-remove {
|
||||
width: 8em;
|
||||
padding-right: 0.5em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.member-remove {
|
||||
text-align: right;
|
||||
|
||||
form button.link-like {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a.member-add {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
form.backend-selection {
|
||||
float: right;
|
||||
|
||||
div.element {
|
||||
margin: 0;
|
||||
|
||||
label {
|
||||
width: auto;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 11.5em;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.usergroupbackend-list {
|
||||
th.backend-remove {
|
||||
width: 8em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td.backend-remove {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
|
@ -310,7 +310,7 @@
|
|||
} else {
|
||||
|
||||
if (req.$target.attr('id') === 'col2') { // TODO: multicol
|
||||
if ($('#col1').data('icingaUrl') === redirect) {
|
||||
if ($('#col1').data('icingaUrl').split('?')[0] === redirect.split('?')[0]) {
|
||||
icinga.ui.layout1col();
|
||||
req.$target = $('#col1');
|
||||
delete(this.requests['col2']);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
/* 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
|
||||
// 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 Icinga\Data\ConfigObject;
|
||||
use Icinga\Test\BaseTestCase;
|
||||
use Icinga\Forms\Config\Authentication\DbBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\DbBackendForm;
|
||||
|
||||
class DbBackendFormTest extends BaseTestCase
|
||||
{
|
||||
|
@ -27,12 +27,12 @@ class DbBackendFormTest extends BaseTestCase
|
|||
public function testValidBackendIsValid()
|
||||
{
|
||||
$this->setUpResourceFactoryMock();
|
||||
Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend')
|
||||
->shouldReceive('count')
|
||||
Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
|
||||
->shouldReceive('select->where->count')
|
||||
->andReturn(2);
|
||||
|
||||
// Passing array(null) is required to make Mockery call the constructor...
|
||||
$form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null));
|
||||
$form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null));
|
||||
$form->shouldReceive('getView->escape')
|
||||
->with(Mockery::type('string'))
|
||||
->andReturnUsing(function ($s) { return $s; });
|
||||
|
@ -41,8 +41,8 @@ class DbBackendFormTest extends BaseTestCase
|
|||
$form->populate(array('resource' => 'test_db_backend'));
|
||||
|
||||
$this->assertTrue(
|
||||
DbBackendForm::isValidAuthenticationBackend($form),
|
||||
'DbBackendForm claims that a valid authentication backend with users is not valid'
|
||||
DbBackendForm::isValidUserBackend($form),
|
||||
'DbBackendForm claims that a valid user backend with users is not valid'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -53,12 +53,12 @@ class DbBackendFormTest extends BaseTestCase
|
|||
public function testInvalidBackendIsNotValid()
|
||||
{
|
||||
$this->setUpResourceFactoryMock();
|
||||
Mockery::mock('overload:Icinga\Authentication\Backend\DbUserBackend')
|
||||
Mockery::mock('overload:Icinga\Authentication\User\DbUserBackend')
|
||||
->shouldReceive('count')
|
||||
->andReturn(0);
|
||||
|
||||
// Passing array(null) is required to make Mockery call the constructor...
|
||||
$form = Mockery::mock('Icinga\Forms\Config\Authentication\DbBackendForm[getView]', array(null));
|
||||
$form = Mockery::mock('Icinga\Forms\Config\UserBackend\DbBackendForm[getView]', array(null));
|
||||
$form->shouldReceive('getView->escape')
|
||||
->with(Mockery::type('string'))
|
||||
->andReturnUsing(function ($s) { return $s; });
|
||||
|
@ -67,8 +67,8 @@ class DbBackendFormTest extends BaseTestCase
|
|||
$form->populate(array('resource' => 'test_db_backend'));
|
||||
|
||||
$this->assertFalse(
|
||||
DbBackendForm::isValidAuthenticationBackend($form),
|
||||
'DbBackendForm claims that an invalid authentication backend without users is valid'
|
||||
DbBackendForm::isValidUserBackend($form),
|
||||
'DbBackendForm claims that an invalid user backend without users is valid'
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
/* 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
|
||||
// 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 Icinga\Data\ConfigObject;
|
||||
use Icinga\Test\BaseTestCase;
|
||||
use Icinga\Forms\Config\Authentication\LdapBackendForm;
|
||||
use Icinga\Forms\Config\UserBackend\LdapBackendForm;
|
||||
use Icinga\Exception\AuthenticationException;
|
||||
|
||||
class LdapBackendFormTest extends BaseTestCase
|
||||
|
@ -28,11 +28,12 @@ class LdapBackendFormTest extends BaseTestCase
|
|||
public function testValidBackendIsValid()
|
||||
{
|
||||
$this->setUpResourceFactoryMock();
|
||||
Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend')
|
||||
->shouldReceive('assertAuthenticationPossible')->andReturnNull();
|
||||
Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
|
||||
->shouldReceive('assertAuthenticationPossible')->andReturnNull()
|
||||
->shouldReceive('setConfig')->andReturnNull();
|
||||
|
||||
// Passing array(null) is required to make Mockery call the constructor...
|
||||
$form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));
|
||||
$form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
|
||||
$form->shouldReceive('getView->escape')
|
||||
->with(Mockery::type('string'))
|
||||
->andReturnUsing(function ($s) { return $s; });
|
||||
|
@ -41,8 +42,8 @@ class LdapBackendFormTest extends BaseTestCase
|
|||
$form->populate(array('resource' => 'test_ldap_backend'));
|
||||
|
||||
$this->assertTrue(
|
||||
LdapBackendForm::isValidAuthenticationBackend($form),
|
||||
'LdapBackendForm claims that a valid authentication backend with users is not valid'
|
||||
LdapBackendForm::isValidUserBackend($form),
|
||||
'LdapBackendForm claims that a valid user backend with users is not valid'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -53,11 +54,11 @@ class LdapBackendFormTest extends BaseTestCase
|
|||
public function testInvalidBackendIsNotValid()
|
||||
{
|
||||
$this->setUpResourceFactoryMock();
|
||||
Mockery::mock('overload:Icinga\Authentication\Backend\LdapUserBackend')
|
||||
Mockery::mock('overload:Icinga\Authentication\User\LdapUserBackend')
|
||||
->shouldReceive('assertAuthenticationPossible')->andThrow(new AuthenticationException);
|
||||
|
||||
// Passing array(null) is required to make Mockery call the constructor...
|
||||
$form = Mockery::mock('Icinga\Forms\Config\Authentication\LdapBackendForm[getView]', array(null));
|
||||
$form = Mockery::mock('Icinga\Forms\Config\UserBackend\LdapBackendForm[getView]', array(null));
|
||||
$form->shouldReceive('getView->escape')
|
||||
->with(Mockery::type('string'))
|
||||
->andReturnUsing(function ($s) { return $s; });
|
||||
|
@ -66,8 +67,8 @@ class LdapBackendFormTest extends BaseTestCase
|
|||
$form->populate(array('resource' => 'test_ldap_backend'));
|
||||
|
||||
$this->assertFalse(
|
||||
LdapBackendForm::isValidAuthenticationBackend($form),
|
||||
'LdapBackendForm claims that an invalid authentication backend without users is valid'
|
||||
LdapBackendForm::isValidUserBackend($form),
|
||||
'LdapBackendForm claims that an invalid user backend without users is valid'
|
||||
);
|
||||
}
|
||||
|
|
@ -5,10 +5,10 @@ namespace Tests\Icinga\Forms\Config;
|
|||
|
||||
use Icinga\Test\BaseTestCase;
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Forms\Config\AuthenticationBackendConfigForm;
|
||||
use Icinga\Forms\Config\AuthenticationBackendReorderForm;
|
||||
use Icinga\Forms\Config\UserBackendConfigForm;
|
||||
use Icinga\Forms\Config\UserBackendReorderForm;
|
||||
|
||||
class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendConfigForm
|
||||
class UserBackendConfigFormWithoutSave extends UserBackendConfigForm
|
||||
{
|
||||
public static $newConfig;
|
||||
|
||||
|
@ -19,11 +19,11 @@ class AuthenticationBackendConfigFormWithoutSave extends AuthenticationBackendCo
|
|||
}
|
||||
}
|
||||
|
||||
class AuthenticationBackendReorderFormProvidingConfigFormWithoutSave extends AuthenticationBackendReorderForm
|
||||
class UserBackendReorderFormProvidingConfigFormWithoutSave extends UserBackendReorderForm
|
||||
{
|
||||
public function getConfigForm()
|
||||
{
|
||||
$form = new AuthenticationBackendConfigFormWithoutSave();
|
||||
$form = new UserBackendConfigFormWithoutSave();
|
||||
$form->setIniConfig($this->config);
|
||||
return $form;
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
|
|||
->shouldReceive('isPost')->andReturn(true)
|
||||
->shouldReceive('getPost')->andReturn(array('backend_newpos' => 'test3|1'));
|
||||
|
||||
$form = new AuthenticationBackendReorderFormProvidingConfigFormWithoutSave();
|
||||
$form = new UserBackendReorderFormProvidingConfigFormWithoutSave();
|
||||
$form->setIniConfig($config);
|
||||
$form->setTokenDisabled();
|
||||
$form->setUidDisabled();
|
||||
|
@ -53,8 +53,8 @@ class AuthenticationBackendReorderFormTest extends BaseTestCase
|
|||
|
||||
$this->assertEquals(
|
||||
array('test1', 'test3', 'test2'),
|
||||
AuthenticationBackendConfigFormWithoutSave::$newConfig->keys(),
|
||||
'Moving elements with AuthenticationBackendReorderForm does not seem to properly work'
|
||||
UserBackendConfigFormWithoutSave::$newConfig->keys(),
|
||||
'Moving elements with UserBackendReorderForm does not seem to properly work'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -60,14 +60,6 @@ class ConfigObjectTest extends BaseTestCase
|
|||
);
|
||||
}
|
||||
|
||||
public function testWhetherConfigObjectsAreCountable()
|
||||
{
|
||||
$config = new ConfigObject(array('a' => 'b', 'c' => array('d' => 'e')));
|
||||
|
||||
$this->assertInstanceOf('Countable', $config, 'ConfigObject objects do not implement interface `Countable\'');
|
||||
$this->assertEquals(2, count($config), 'ConfigObject objects do not count properties and sections correctly');
|
||||
}
|
||||
|
||||
public function testWhetherConfigObjectsAreTraversable()
|
||||
{
|
||||
$config = new ConfigObject(array('a' => 'b', 'c' => 'd'));
|
||||
|
@ -124,7 +116,7 @@ class ConfigObjectTest extends BaseTestCase
|
|||
}
|
||||
|
||||
/**
|
||||
* @expectedException LogicException
|
||||
* @expectedException \Icinga\Exception\ProgrammingError
|
||||
*/
|
||||
public function testWhetherItIsNotPossibleToAppendProperties()
|
||||
{
|
||||
|
@ -142,9 +134,6 @@ class ConfigObjectTest extends BaseTestCase
|
|||
$this->assertFalse(isset($config->c), 'ConfigObjects do not allow to unset sections');
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testWhetherConfigObjectsAreCountable
|
||||
*/
|
||||
public function testWhetherOneCanCheckIfAConfigObjectHasAnyPropertiesOrSections()
|
||||
{
|
||||
$config = new ConfigObject();
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
|
@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase
|
|||
return $select;
|
||||
}
|
||||
|
||||
public function testLimit()
|
||||
{
|
||||
$select = $this->prepareSelect();
|
||||
$this->assertEquals(10, $select->getLimit());
|
||||
$this->assertEquals(4, $select->getOffset());
|
||||
}
|
||||
|
||||
public function testHasLimit()
|
||||
{
|
||||
$select = $this->emptySelect();
|
||||
$this->assertFalse($select->hasLimit());
|
||||
$select = $this->prepareSelect();
|
||||
$this->assertTrue($select->hasLimit());
|
||||
}
|
||||
|
||||
public function testHasOffset()
|
||||
{
|
||||
$select = $this->emptySelect();
|
||||
$this->assertFalse($select->hasOffset());
|
||||
$select = $this->prepareSelect();
|
||||
$this->assertTrue($select->hasOffset());
|
||||
}
|
||||
|
||||
public function testGetLimit()
|
||||
{
|
||||
$select = $this->prepareSelect();
|
||||
$this->assertEquals(10, $select->getLimit());
|
||||
}
|
||||
|
||||
public function testGetOffset()
|
||||
{
|
||||
$select = $this->prepareSelect();
|
||||
$this->assertEquals(10, $select->getLimit());
|
||||
}
|
||||
|
||||
public function testFetchTree()
|
||||
{
|
||||
$this->markTestIncomplete('testFetchTree is not implemented yet - requires real LDAP');
|
||||
}
|
||||
|
||||
public function testFrom()
|
||||
{
|
||||
return $this->testListFields();
|
||||
}
|
||||
|
||||
public function testWhere()
|
||||
{
|
||||
$this->markTestIncomplete('testWhere is not implemented yet');
|
||||
|
@ -88,30 +48,13 @@ class QueryTest extends BaseTestCase
|
|||
|
||||
public function testOrder()
|
||||
{
|
||||
$select = $this->emptySelect()->order('bla');
|
||||
// tested by testGetSortColumns
|
||||
$this->markTestIncomplete('testOrder is not implemented yet, order support for ldap queries is incomplete');
|
||||
}
|
||||
|
||||
public function testListFields()
|
||||
{
|
||||
$select = $this->prepareSelect();
|
||||
$this->assertEquals(
|
||||
array('testIntColumn', 'testStringColumn'),
|
||||
$select->listFields()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetSortColumns()
|
||||
{
|
||||
$select = $this->prepareSelect();
|
||||
$cols = $select->getSortColumns();
|
||||
$this->assertEquals('testIntColumn', $cols[0][0]);
|
||||
}
|
||||
|
||||
public function testCreateQuery()
|
||||
public function testRenderFilter()
|
||||
{
|
||||
$select = $this->prepareSelect();
|
||||
$res = '(&(objectClass=dummyClass)(testIntColumn=1)(testStringColumn=test)(testWildcard=abc*))';
|
||||
$this->assertEquals($res, $select->create());
|
||||
$this->assertEquals($res, (string) $select);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,13 +67,15 @@ class UserTest extends BaseTestCase
|
|||
'test',
|
||||
'test/some/specific',
|
||||
'test/more/*',
|
||||
'test/wildcard-with-wildcard/*'
|
||||
'test/wildcard-with-wildcard/*',
|
||||
'test/even-more/specific-with-wildcard/*'
|
||||
));
|
||||
$this->assertTrue($user->can('test'));
|
||||
$this->assertTrue($user->can('test/some/specific'));
|
||||
$this->assertTrue($user->can('test/more/everything'));
|
||||
$this->assertTrue($user->can('test/wildcard-with-wildcard/*'));
|
||||
$this->assertTrue($user->can('test/wildcard-with-wildcard/sub/sub'));
|
||||
$this->assertTrue($user->can('test/even-more/*'));
|
||||
$this->assertFalse($user->can('not/test'));
|
||||
$this->assertFalse($user->can('test/some/not/so/specific'));
|
||||
$this->assertFalse($user->can('test/wildcard2/*'));
|
||||
|
|
Loading…
Reference in New Issue