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

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

View File

@ -7,7 +7,7 @@ use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Application\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;

View File

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

View File

@ -0,0 +1,345 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Exception;
use Icinga\Application\Logger;
use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\Data\Reducible;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\Config\UserGroup\AddMemberForm;
use Icinga\Forms\Config\UserGroup\UserGroupForm;
use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\Form;
use Icinga\Web\Notification;
use Icinga\Web\Url;
use Icinga\Web\Widget;
class GroupController extends AuthBackendController
{
/**
* List all user groups of a single backend
*/
public function listAction()
{
$this->assertPermission('config/authentication/groups/show');
$backendNames = array_map(
function ($b) { return $b->getName(); },
$this->loadUserGroupBackends('Icinga\Data\Selectable')
);
$this->view->backendSelection = new Form();
$this->view->backendSelection->setAttrib('class', 'backend-selection');
$this->view->backendSelection->setUidDisabled();
$this->view->backendSelection->setMethod('GET');
$this->view->backendSelection->setTokenDisabled();
$this->view->backendSelection->addElement(
'select',
'backend',
array(
'autosubmit' => true,
'label' => $this->translate('Usergroup Backend'),
'multiOptions' => array_combine($backendNames, $backendNames),
'value' => $this->params->get('backend')
)
);
$backend = $this->getUserGroupBackend($this->params->get('backend'));
if ($backend === null) {
$this->view->backend = null;
return;
}
$query = $backend->select(array('group_name'));
$filterEditor = Widget::create('filterEditor')
->setQuery($query)
->preserveParams('limit', 'sort', 'dir', 'view', 'backend')
->ignoreParams('page')
->handleRequest($this->getRequest());
$query->applyFilter($filterEditor->getFilter());
$this->setupFilterControl($filterEditor);
$this->view->groups = $query;
$this->view->backend = $backend;
$this->createListTabs()->activate('group/list');
$this->setupPaginationControl($query);
$this->setupLimitControl();
$this->setupSortControl(
array(
'group_name' => $this->translate('Group'),
'created_at' => $this->translate('Created at'),
'last_modified' => $this->translate('Last modified')
),
$query
);
}
/**
* Show a group
*/
public function showAction()
{
$this->assertPermission('config/authentication/groups/show');
$groupName = $this->params->getRequired('group');
$backend = $this->getUserGroupBackend($this->params->getRequired('backend'));
$group = $backend->select(array(
'group_name',
'created_at',
'last_modified'
))->where('group_name', $groupName)->fetchRow();
if ($group === false) {
$this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
}
$members = $backend
->select()
->from('group_membership', array('user_name'))
->where('group_name', $groupName);
$filterEditor = Widget::create('filterEditor')
->setQuery($members)
->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'group')
->ignoreParams('page')
->handleRequest($this->getRequest());
$members->applyFilter($filterEditor->getFilter());
$this->setupFilterControl($filterEditor);
$this->setupPaginationControl($members);
$this->setupLimitControl();
$this->setupSortControl(
array(
'user_name' => $this->translate('Username'),
'created_at' => $this->translate('Created at'),
'last_modified' => $this->translate('Last modified')
),
$members
);
$this->view->group = $group;
$this->view->backend = $backend;
$this->view->members = $members;
$this->createShowTabs($backend->getName(), $groupName)->activate('group/show');
if ($this->hasPermission('config/authentication/groups/edit') && $backend instanceof Reducible) {
$removeForm = new Form();
$removeForm->setUidDisabled();
$removeForm->setAction(
Url::fromPath('group/removemember', array('backend' => $backend->getName(), 'group' => $groupName))
);
$removeForm->addElement('hidden', 'user_name', array(
'isArray' => true,
'decorators' => array('ViewHelper')
));
$removeForm->addElement('hidden', 'redirect', array(
'value' => Url::fromPath('group/show', array(
'backend' => $backend->getName(),
'group' => $groupName
)),
'decorators' => array('ViewHelper')
));
$removeForm->addElement('button', 'btn_submit', array(
'escape' => false,
'type' => 'submit',
'class' => 'link-like',
'value' => 'btn_submit',
'decorators' => array('ViewHelper'),
'label' => $this->view->icon('trash'),
'title' => $this->translate('Remove this member')
));
$this->view->removeForm = $removeForm;
}
}
/**
* Add a group
*/
public function addAction()
{
$this->assertPermission('config/authentication/groups/add');
$backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
$form = new UserGroupForm();
$form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
$form->setRepository($backend);
$form->add()->handleRequest();
$this->view->form = $form;
$this->render('form');
}
/**
* Edit a group
*/
public function editAction()
{
$this->assertPermission('config/authentication/groups/edit');
$groupName = $this->params->getRequired('group');
$backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
$form = new UserGroupForm();
$form->setRedirectUrl(
Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
);
$form->setRepository($backend);
try {
$form->edit($groupName)->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
}
$this->view->form = $form;
$this->render('form');
}
/**
* Remove a group
*/
public function removeAction()
{
$this->assertPermission('config/authentication/groups/remove');
$groupName = $this->params->getRequired('group');
$backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
$form = new UserGroupForm();
$form->setRedirectUrl(Url::fromPath('group/list', array('backend' => $backend->getName())));
$form->setRepository($backend);
try {
$form->remove($groupName)->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
}
$this->view->form = $form;
$this->render('form');
}
/**
* Add a group member
*/
public function addmemberAction()
{
$this->assertPermission('config/authentication/groups/edit');
$groupName = $this->params->getRequired('group');
$backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
$form = new AddMemberForm();
$form->setDataSource($this->fetchUsers())
->setBackend($backend)
->setGroupName($groupName)
->setRedirectUrl(
Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName))
)
->setUidDisabled();
try {
$form->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
}
$this->view->form = $form;
$this->render('form');
}
/**
* Remove a group member
*/
public function removememberAction()
{
$this->assertPermission('config/authentication/groups/edit');
$this->assertHttpMethod('POST');
$groupName = $this->params->getRequired('group');
$backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
$form = new Form(array(
'onSuccess' => function ($form) use ($groupName, $backend) {
foreach ($form->getValue('user_name') as $userName) {
try {
$backend->delete(
'group_membership',
Filter::matchAll(
Filter::where('group_name', $groupName),
Filter::where('user_name', $userName)
)
);
Notification::success(sprintf(
t('User "%s" has been removed from group "%s"'),
$userName,
$groupName
));
} catch (NotFoundError $e) {
throw $e;
} catch (Exception $e) {
Notification::error($e->getMessage());
}
}
$redirect = $form->getValue('redirect');
if (! empty($redirect)) {
$form->setRedirectUrl(htmlspecialchars_decode($redirect));
}
return true;
}
));
$form->setUidDisabled();
$form->setSubmitLabel('btn_submit'); // Required to ensure that isSubmitted() is called
$form->addElement('hidden', 'user_name', array('required' => true, 'isArray' => true));
$form->addElement('hidden', 'redirect');
try {
$form->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName));
}
}
/**
* Fetch and return all users from all user backends
*
* @return ArrayDatasource
*/
protected function fetchUsers()
{
$users = array();
foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) {
try {
foreach ($backend->select(array('user_name')) as $row) {
$users[] = $row;
}
} catch (Exception $e) {
Logger::error($e);
Notification::warning(sprintf(
$this->translate('Failed to fetch any users from backend %s. Please check your log'),
$backend->getName()
));
}
}
return new ArrayDatasource($users);
}
/**
* Create the tabs to display when showing a group
*
* @param string $backendName
* @param string $groupName
*/
protected function createShowTabs($backendName, $groupName)
{
$tabs = $this->getTabs();
$tabs->add(
'group/show',
array(
'title' => sprintf($this->translate('Show group %s'), $groupName),
'label' => $this->translate('Group'),
'icon' => 'users',
'url' => Url::fromPath('group/show', array('backend' => $backendName, 'group' => $groupName))
)
);
return $tabs;
}
}

View File

@ -4,69 +4,27 @@
use Icinga\Application\Config;
use Icinga\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');
}
}

View File

@ -0,0 +1,304 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Exception;
use Icinga\Application\Logger;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\Config\User\CreateMembershipForm;
use Icinga\Forms\Config\User\UserForm;
use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\User;
use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\Form;
use Icinga\Web\Notification;
use Icinga\Web\Url;
use Icinga\Web\Widget;
class UserController extends AuthBackendController
{
/**
* List all users of a single backend
*/
public function listAction()
{
$this->assertPermission('config/authentication/users/show');
$backendNames = array_map(
function ($b) { return $b->getName(); },
$this->loadUserBackends('Icinga\Data\Selectable')
);
$this->view->backendSelection = new Form();
$this->view->backendSelection->setAttrib('class', 'backend-selection');
$this->view->backendSelection->setUidDisabled();
$this->view->backendSelection->setMethod('GET');
$this->view->backendSelection->setTokenDisabled();
$this->view->backendSelection->addElement(
'select',
'backend',
array(
'autosubmit' => true,
'label' => $this->translate('Authentication Backend'),
'multiOptions' => array_combine($backendNames, $backendNames),
'value' => $this->params->get('backend')
)
);
$backend = $this->getUserBackend($this->params->get('backend'));
if ($backend === null) {
$this->view->backend = null;
return;
}
$query = $backend->select(array('user_name'));
$filterEditor = Widget::create('filterEditor')
->setQuery($query)
->preserveParams('limit', 'sort', 'dir', 'view', 'backend')
->ignoreParams('page')
->handleRequest($this->getRequest());
$query->applyFilter($filterEditor->getFilter());
$this->setupFilterControl($filterEditor);
$this->view->users = $query;
$this->view->backend = $backend;
$this->createListTabs()->activate('user/list');
$this->setupPaginationControl($query);
$this->setupLimitControl();
$this->setupSortControl(
array(
'user_name' => $this->translate('Username'),
'is_active' => $this->translate('Active'),
'created_at' => $this->translate('Created at'),
'last_modified' => $this->translate('Last modified')
),
$query
);
}
/**
* Show a user
*/
public function showAction()
{
$this->assertPermission('config/authentication/users/show');
$userName = $this->params->getRequired('user');
$backend = $this->getUserBackend($this->params->getRequired('backend'));
$user = $backend->select(array(
'user_name',
'is_active',
'created_at',
'last_modified'
))->where('user_name', $userName)->fetchRow();
if ($user === false) {
$this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
}
$memberships = $this->loadMemberships(new User($userName))->select();
$filterEditor = Widget::create('filterEditor')
->setQuery($memberships)
->preserveParams('limit', 'sort', 'dir', 'view', 'backend', 'user')
->ignoreParams('page')
->handleRequest($this->getRequest());
$memberships->applyFilter($filterEditor->getFilter());
$this->setupFilterControl($filterEditor);
$this->setupPaginationControl($memberships);
$this->setupLimitControl();
$this->setupSortControl(
array(
'group_name' => $this->translate('Group')
),
$memberships
);
if ($this->hasPermission('config/authentication/groups/edit')) {
$extensibleBackends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
$this->view->showCreateMembershipLink = ! empty($extensibleBackends);
} else {
$this->view->showCreateMembershipLink = false;
}
$this->view->user = $user;
$this->view->backend = $backend;
$this->view->memberships = $memberships;
$this->createShowTabs($backend->getName(), $userName)->activate('user/show');
if ($this->hasPermission('config/authentication/groups/edit')) {
$removeForm = new Form();
$removeForm->setUidDisabled();
$removeForm->addElement('hidden', 'user_name', array(
'isArray' => true,
'value' => $userName,
'decorators' => array('ViewHelper')
));
$removeForm->addElement('hidden', 'redirect', array(
'value' => Url::fromPath('user/show', array(
'backend' => $backend->getName(),
'user' => $userName
)),
'decorators' => array('ViewHelper')
));
$removeForm->addElement('button', 'btn_submit', array(
'escape' => false,
'type' => 'submit',
'class' => 'link-like',
'value' => 'btn_submit',
'decorators' => array('ViewHelper'),
'label' => $this->view->icon('trash'),
'title' => $this->translate('Cancel this membership')
));
$this->view->removeForm = $removeForm;
}
}
/**
* Add a user
*/
public function addAction()
{
$this->assertPermission('config/authentication/users/add');
$backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible');
$form = new UserForm();
$form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
$form->setRepository($backend);
$form->add()->handleRequest();
$this->view->form = $form;
$this->render('form');
}
/**
* Edit a user
*/
public function editAction()
{
$this->assertPermission('config/authentication/users/edit');
$userName = $this->params->getRequired('user');
$backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Updatable');
$form = new UserForm();
$form->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)));
$form->setRepository($backend);
try {
$form->edit($userName)->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
}
$this->view->form = $form;
$this->render('form');
}
/**
* Remove a user
*/
public function removeAction()
{
$this->assertPermission('config/authentication/users/remove');
$userName = $this->params->getRequired('user');
$backend = $this->getUserBackend($this->params->getRequired('backend'), 'Icinga\Data\Reducible');
$form = new UserForm();
$form->setRedirectUrl(Url::fromPath('user/list', array('backend' => $backend->getName())));
$form->setRepository($backend);
try {
$form->remove($userName)->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
}
$this->view->form = $form;
$this->render('form');
}
/**
* Create a membership for a user
*/
public function createmembershipAction()
{
$this->assertPermission('config/authentication/groups/edit');
$userName = $this->params->getRequired('user');
$backend = $this->getUserBackend($this->params->getRequired('backend'));
if ($backend->select()->where('user_name', $userName)->count() === 0) {
$this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
}
$backends = $this->loadUserGroupBackends('Icinga\Data\Extensible');
if (empty($backends)) {
throw new ConfigurationError($this->translate(
'You\'ll need to configure at least one user group backend first that allows to create new memberships'
));
}
$form = new CreateMembershipForm();
$form->setBackends($backends)
->setUsername($userName)
->setRedirectUrl(Url::fromPath('user/show', array('backend' => $backend->getName(), 'user' => $userName)))
->handleRequest();
$this->view->form = $form;
$this->render('form');
}
/**
* Fetch and return the given user's groups from all user group backends
*
* @param User $user
*
* @return ArrayDatasource
*/
protected function loadMemberships(User $user)
{
$groups = $alreadySeen = array();
foreach ($this->loadUserGroupBackends() as $backend) {
try {
foreach ($backend->getMemberships($user) as $groupName) {
if (array_key_exists($groupName, $alreadySeen)) {
continue; // Ignore duplicate memberships
}
$alreadySeen[$groupName] = null;
$groups[] = (object) array(
'group_name' => $groupName,
'backend' => $backend
);
}
} catch (Exception $e) {
Logger::error($e);
Notification::warning(sprintf(
$this->translate('Failed to fetch memberships from backend %s. Please check your log'),
$backend->getName()
));
}
}
return new ArrayDatasource($groups);
}
/**
* Create the tabs to display when showing a user
*
* @param string $backendName
* @param string $userName
*/
protected function createShowTabs($backendName, $userName)
{
$tabs = $this->getTabs();
$tabs->add(
'user/show',
array(
'title' => sprintf($this->translate('Show user %s'), $userName),
'label' => $this->translate('User'),
'icon' => 'user',
'url' => Url::fromPath('user/show', array('backend' => $backendName, 'user' => $userName))
)
);
return $tabs;
}
}

View File

@ -0,0 +1,183 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Exception;
use Icinga\Application\Config;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\ConfirmRemovalForm;
use Icinga\Forms\Config\UserGroup\UserGroupBackendForm;
use Icinga\Web\Controller;
use Icinga\Web\Notification;
use Icinga\Web\Url;
/**
* Controller to configure user group backends
*/
class UsergroupbackendController extends Controller
{
/**
* Initialize this controller
*/
public function init()
{
$this->assertPermission('config/application/usergroupbackend');
}
/**
* Redirect to this controller's list action
*/
public function indexAction()
{
$this->redirectNow('usergroupbackend/list');
}
/**
* Show a list of all user group backends
*/
public function listAction()
{
$this->view->backendNames = Config::app('groups')->keys();
$this->createListTabs()->activate('usergroupbackend');
}
/**
* Create a new user group backend
*/
public function createAction()
{
$form = new UserGroupBackendForm();
$form->setRedirectUrl('usergroupbackend/list');
$form->setTitle($this->translate('Create New User Group Backend'));
$form->addDescription($this->translate('Create a new backend to associate users and groups with.'));
$form->setIniConfig(Config::app('groups'));
$form->setOnSuccess(function (UserGroupBackendForm $form) {
try {
$form->add($form->getValues());
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
}
if ($form->save()) {
Notification::success(t('User group backend successfully created'));
return true;
}
return false;
});
$form->handleRequest();
$this->view->form = $form;
$this->render('form');
}
/**
* Edit an user group backend
*/
public function editAction()
{
$backendName = $this->params->getRequired('backend');
$form = new UserGroupBackendForm();
$form->setAction(Url::fromRequest());
$form->setRedirectUrl('usergroupbackend/list');
$form->setTitle(sprintf($this->translate('Edit User Group Backend %s'), $backendName));
$form->setIniConfig(Config::app('groups'));
$form->setOnSuccess(function (UserGroupBackendForm $form) use ($backendName) {
try {
$form->edit($backendName, $form->getValues());
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
}
if ($form->save()) {
Notification::success(sprintf(t('User group backend "%s" successfully updated'), $backendName));
return true;
}
return false;
});
try {
$form->load($backendName);
$form->handleRequest();
} catch (NotFoundError $_) {
$this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $backendName));
}
$this->view->form = $form;
$this->render('form');
}
/**
* Remove a user group backend
*/
public function removeAction()
{
$backendName = $this->params->getRequired('backend');
$backendForm = new UserGroupBackendForm();
$backendForm->setIniConfig(Config::app('groups'));
$form = new ConfirmRemovalForm();
$form->setRedirectUrl('usergroupbackend/list');
$form->setTitle(sprintf($this->translate('Remove User Group Backend %s'), $backendName));
$form->setOnSuccess(function (ConfirmRemovalForm $form) use ($backendName, $backendForm) {
try {
$backendForm->delete($backendName);
} catch (Exception $e) {
$form->error($e->getMessage());
return false;
}
if ($backendForm->save()) {
Notification::success(sprintf(t('User group backend "%s" successfully removed'), $backendName));
return true;
}
return false;
});
$form->handleRequest();
$this->view->form = $form;
$this->render('form');
}
/**
* Create the tabs for the application configuration
*/
protected function createListTabs()
{
$tabs = $this->getTabs();
if ($this->hasPermission('config/application/general')) {
$tabs->add('general', array(
'title' => $this->translate('Adjust the general configuration of Icinga Web 2'),
'label' => $this->translate('General'),
'url' => 'config/general'
));
}
if ($this->hasPermission('config/application/resources')) {
$tabs->add('resource', array(
'title' => $this->translate('Configure which resources are being utilized by Icinga Web 2'),
'label' => $this->translate('Resources'),
'url' => 'config/resource'
));
}
if ($this->hasPermission('config/application/userbackend')) {
$tabs->add('userbackend', array(
'title' => $this->translate('Configure how users authenticate with and log into Icinga Web 2'),
'label' => $this->translate('Authentication'),
'url' => 'config/userbackend'
));
}
if ($this->hasPermission('config/application/usergroupbackend')) {
$tabs->add('usergroupbackend', array(
'title' => $this->translate('Configure how users are associated with groups by Icinga Web 2'),
'label' => $this->translate('User Groups'),
'url' => 'usergroupbackend/list'
));
}
return $tabs;
}
}

View File

@ -0,0 +1,191 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\User;
use Exception;
use Icinga\Application\Logger;
use Icinga\Data\DataArray\ArrayDatasource;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form for creating one or more group memberships
*/
class CreateMembershipForm extends Form
{
/**
* The user group backends to fetch groups from
*
* Each backend must implement the Icinga\Data\Extensible and Icinga\Data\Selectable interface.
*
* @var array
*/
protected $backends;
/**
* The username to create memberships for
*
* @var string
*/
protected $userName;
/**
* Set the user group backends to fetch groups from
*
* @param array $backends
*
* @return $this
*/
public function setBackends($backends)
{
$this->backends = $backends;
return $this;
}
/**
* Set the username to create memberships for
*
* @param string $userName
*
* @return $this
*/
public function setUsername($userName)
{
$this->userName = $userName;
return $this;
}
/**
* Create and add elements to this form
*
* @param array $formData The data sent by the user
*/
public function createElements(array $formData)
{
$query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name'));
$options = array();
foreach ($query as $row) {
$options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')';
}
$this->addElement(
'multiselect',
'groups',
array(
'required' => true,
'multiOptions' => $options,
'label' => $this->translate('Groups'),
'description' => sprintf(
$this->translate('Select one or more groups where to add %s as member'),
$this->userName
),
'class' => 'grant-permissions'
)
);
$this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName));
$this->setSubmitLabel($this->translate('Create'));
}
/**
* Instantly redirect back in case the user is already a member of all groups
*/
public function onRequest()
{
if ($this->createDataSource()->select()->from('group')->count() === 0) {
Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName));
$this->getResponse()->redirectAndExit($this->getRedirectUrl());
}
}
/**
* Create the memberships for the user
*
* @return bool
*/
public function onSuccess()
{
$backendMap = array();
foreach ($this->backends as $backend) {
$backendMap[$backend->getName()] = $backend;
}
$single = null;
foreach ($this->getValue('groups') as $backendAndGroup) {
list($backendName, $groupName) = explode(';', $backendAndGroup, 2);
try {
$backendMap[$backendName]->insert(
'group_membership',
array(
'group_name' => $groupName,
'user_name' => $this->userName
)
);
} catch (Exception $e) {
Notification::error(sprintf(
$this->translate('Failed to add "%s" as group member for "%s"'),
$this->userName,
$groupName
));
$this->error($e->getMessage());
return false;
}
$single = $single === null;
}
if ($single) {
Notification::success(
sprintf($this->translate('Membership for group %s created successfully'), $groupName)
);
} else {
Notification::success($this->translate('Memberships created successfully'));
}
return true;
}
/**
* Create and return a data source to fetch all groups from all backends where the user is not already a member of
*
* @return ArrayDatasource
*/
protected function createDataSource()
{
$groups = $failures = array();
foreach ($this->backends as $backend) {
try {
$memberships = $backend
->select()
->from('group_membership', array('group_name'))
->where('user_name', $this->userName)
->fetchColumn();
foreach ($backend->select(array('group_name')) as $row) {
if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter
$row->backend_name = $backend->getName();
$groups[] = $row;
}
}
} catch (Exception $e) {
$failures[] = array($backend->getName(), $e);
}
}
if (empty($groups) && !empty($failures)) {
// In case there are only failures, throw the very first exception again
throw $failures[0][1];
} elseif (! empty($failures)) {
foreach ($failures as $failure) {
Logger::error($failure[1]);
Notification::warning(sprintf(
$this->translate('Failed to fetch any groups from backend %s. Please check your log'),
$failure[0]
));
}
}
return new ArrayDatasource($groups);
}
}

View File

@ -0,0 +1,175 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\User;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\RepositoryForm;
class UserForm extends RepositoryForm
{
/**
* Create and add elements to this form to insert or update a user
*
* @param array $formData The data sent by the user
*/
protected function createInsertElements(array $formData)
{
$this->addElement(
'checkbox',
'is_active',
array(
'required' => true,
'value' => true,
'label' => $this->translate('Active'),
'description' => $this->translate('Prevents the user from logging in if unchecked')
)
);
$this->addElement(
'text',
'user_name',
array(
'required' => true,
'label' => $this->translate('Username')
)
);
$this->addElement(
'text',
'password',
array(
'required' => true,
'label' => $this->translate('Password')
)
);
$this->setTitle($this->translate('Add a new user'));
$this->setSubmitLabel($this->translate('Add'));
}
/**
* Create and add elements to this form to update a user
*
* @param array $formData The data sent by the user
*/
protected function createUpdateElements(array $formData)
{
$this->createInsertElements($formData);
$this->addElement(
'text',
'password',
array(
'label' => $this->translate('Password')
)
);
$this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
$this->setSubmitLabel($this->translate('Save'));
}
/**
* Update a user
*
* @return bool
*/
protected function onUpdateSuccess()
{
if (parent::onUpdateSuccess()) {
if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) {
$this->getRedirectUrl()->setParam('user', $newName);
}
return true;
}
return false;
}
/**
* Retrieve all form element values
*
* Strips off the password if null or the empty string.
*
* @param bool $suppressArrayNotation
*
* @return array
*/
public function getValues($suppressArrayNotation = false)
{
$values = parent::getValues($suppressArrayNotation);
if (! $values['password']) {
unset($values['password']);
}
return $values;
}
/**
* Create and add elements to this form to delete a user
*
* @param array $formData The data sent by the user
*/
protected function createDeleteElements(array $formData)
{
$this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier()));
$this->setSubmitLabel($this->translate('Yes'));
}
/**
* Create and return a filter to use when updating or deleting a user
*
* @return Filter
*/
protected function createFilter()
{
return Filter::where('user_name', $this->getIdentifier());
}
/**
* Return a notification message to use when inserting a user
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
protected function getInsertMessage($success)
{
if ($success) {
return $this->translate('User added successfully');
} else {
return $this->translate('Failed to add user');
}
}
/**
* Return a notification message to use when updating a user
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
protected function getUpdateMessage($success)
{
if ($success) {
return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier());
} else {
return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier());
}
}
/**
* Return a notification message to use when deleting a user
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
protected function getDeleteMessage($success)
{
if ($success) {
return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier());
} else {
return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier());
}
}
}

View File

@ -1,16 +1,16 @@
<?php
/* 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) {

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,182 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Exception;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Selectable;
use Icinga\Exception\NotFoundError;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form for adding one or more group members
*/
class AddMemberForm extends Form
{
/**
* The data source to fetch users from
*
* @var Selectable
*/
protected $ds;
/**
* The user group backend to use
*
* @var Extensible
*/
protected $backend;
/**
* The group to add members for
*
* @var string
*/
protected $groupName;
/**
* Set the data source to fetch users from
*
* @param Selectable $ds
*
* @return $this
*/
public function setDataSource(Selectable $ds)
{
$this->ds = $ds;
return $this;
}
/**
* Set the user group backend to use
*
* @param Extensible $backend
*
* @return $this
*/
public function setBackend(Extensible $backend)
{
$this->backend = $backend;
return $this;
}
/**
* Set the group to add members for
*
* @param string $groupName
*
* @return $this
*/
public function setGroupName($groupName)
{
$this->groupName = $groupName;
return $this;
}
/**
* Create and add elements to this form
*
* @param array $formData The data sent by the user
*/
public function createElements(array $formData)
{
// TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate
// memberships (no matter whether the data source permits it or not, a member does never need to be
// added more than once) should be kept at backend level (GroupController::fetchUsers) but this does
// not work currently as our ldap protocol stuff is unable to handle our filter implementation..
$members = $this->backend
->select()
->from('group_membership', array('user_name'))
->where('group_name', $this->groupName)
->fetchColumn();
$filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members));
$users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn();
if (! empty($users)) {
$this->addElement(
'multiselect',
'user_name',
array(
'multiOptions' => array_combine($users, $users),
'label' => $this->translate('Backend Users'),
'description' => $this->translate(
'Select one or more users (fetched from your user backends) to add as group member'
),
'class' => 'grant-permissions'
)
);
}
$this->addElement(
'textarea',
'users',
array(
'required' => empty($users),
'label' => $this->translate('Users'),
'description' => $this->translate(
'Provide one or more usernames separated by comma to add as group member'
)
)
);
$this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName));
$this->setSubmitLabel($this->translate('Add'));
}
/**
* Insert the members for the group
*
* @return bool
*/
public function onSuccess()
{
$userNames = $this->getValue('user_name') ?: array();
if (($users = $this->getValue('users'))) {
$userNames = array_merge($userNames, array_map('trim', explode(',', $users)));
}
if (empty($userNames)) {
$this->info($this->translate(
'Please provide at least one username, either by choosing one '
. 'in the list or by manually typing one in the text box below'
));
return false;
}
$single = null;
foreach ($userNames as $userName) {
try {
$this->backend->insert(
'group_membership',
array(
'group_name' => $this->groupName,
'user_name' => $userName
)
);
} catch (NotFoundError $e) {
throw $e; // Trigger 404, the group name is initially accessed as GET parameter
} catch (Exception $e) {
Notification::error(sprintf(
$this->translate('Failed to add "%s" as group member for "%s"'),
$userName,
$this->groupName
));
$this->error($e->getMessage());
return false;
}
$single = $single === null;
}
if ($single) {
Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName));
} else {
Notification::success($this->translate('Group members added successfully'));
}
return true;
}
}

View File

@ -0,0 +1,58 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Icinga\Data\ResourceFactory;
use Icinga\Web\Form;
/**
* Form for managing database user group backends
*/
class DbUserGroupBackendForm extends Form
{
/**
* Initialize this form
*/
public function init()
{
$this->setName('form_config_dbusergroupbackend');
}
/**
* Create and add elements to this form
*
* @param array $formData
*/
public function createElements(array $formData)
{
$resourceNames = $this->getDatabaseResourceNames();
$this->addElement(
'select',
'resource',
array(
'required' => true,
'label' => $this->translate('Database Connection'),
'description' => $this->translate('The database connection to use for this backend'),
'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames)
)
);
}
/**
* Return the names of all configured database resources
*
* @return array
*/
protected function getDatabaseResourceNames()
{
$names = array();
foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
if (strtolower($config->type) === 'db') {
$names[] = $name;
}
}
return $names;
}
}

View File

@ -0,0 +1,198 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use InvalidArgumentException;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\ConfigForm;
/**
* Form for managing user group backends
*/
class UserGroupBackendForm extends ConfigForm
{
/**
* Initialize this form
*/
public function init()
{
$this->setName('form_config_usergroupbackend');
$this->setSubmitLabel($this->translate('Save Changes'));
}
/**
* Return a form object for the given backend type
*
* @param string $type The backend type for which to return a form
*
* @return Form
*/
public function getBackendForm($type)
{
if ($type === 'db') {
return new DbUserGroupBackendForm();
} else {
throw new InvalidArgumentException(sprintf($this->translate('Invalid backend type "%s" provided'), $type));
}
}
/**
* Populate the form with the given backend's config
*
* @param string $name
*
* @return $this
*
* @throws NotFoundError In case no backend with the given name is found
*/
public function load($name)
{
if (! $this->config->hasSection($name)) {
throw new NotFoundError('No user group backend called "%s" found', $name);
}
$data = $this->config->getSection($name)->toArray();
$data['type'] = $data['backend'];
$data['name'] = $name;
$this->populate($data);
return $this;
}
/**
* Add a new user group backend
*
* @param array $data
*
* @return $this
*
* @throws InvalidArgumentException In case $data does not contain a backend name
* @throws IcingaException In case a backend with the same name already exists
*/
public function add(array $data)
{
if (! isset($data['name'])) {
throw new InvalidArgumentException('Key \'name\' missing');
}
$backendName = $data['name'];
if ($this->config->hasSection($backendName)) {
throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName);
}
unset($data['name']);
$this->config->setSection($backendName, $data);
return $this;
}
/**
* Edit a user group backend
*
* @param string $name
* @param array $data
*
* @return $this
*
* @throws NotFoundError In case no backend with the given name is found
*/
public function edit($name, array $data)
{
if (! $this->config->hasSection($name)) {
throw new NotFoundError('No user group backend called "%s" found', $name);
}
$backendConfig = $this->config->getSection($name);
if (isset($data['name']) && $data['name'] !== $name) {
$this->config->removeSection($name);
$name = $data['name'];
unset($data['name']);
}
$this->config->setSection($name, $backendConfig->merge($data));
return $this;
}
/**
* Remove a user group backend
*
* @param string $name
*
* @return $this
*/
public function delete($name)
{
$this->config->removeSection($name);
return $this;
}
/**
* Create and add elements to this form
*
* @param array $formData
*/
public function createElements(array $formData)
{
$this->addElement(
'text',
'name',
array(
'required' => true,
'label' => $this->translate('Backend Name'),
'description' => $this->translate(
'The name of this user group backend that is used to differentiate it from others'
),
'validators' => array(
array(
'Regex',
false,
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => $this->translate(
'The backend name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
)
)
);
// TODO(jom): We did not think about how to configure custom group backends yet!
$backendTypes = array(
'db' => $this->translate('Database')
);
$backendType = isset($formData['type']) ? $formData['type'] : null;
if ($backendType === null) {
$backendType = key($backendTypes);
}
$this->addElement(
'hidden',
'backend',
array(
'disabled' => true, // Prevents the element from being submitted, see #7717
'value' => $backendType
)
);
$this->addElement(
'select',
'type',
array(
'ignore' => true,
'required' => true,
'autosubmit' => true,
'label' => $this->translate('Backend Type'),
'description' => $this->translate('The type of this user group backend'),
'multiOptions' => $backendTypes
)
);
$backendForm = $this->getBackendForm($backendType);
$backendForm->createElements($formData);
$this->addElements($backendForm->getElements());
}
}

View File

@ -0,0 +1,126 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms\Config\UserGroup;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\RepositoryForm;
class UserGroupForm extends RepositoryForm
{
/**
* Create and add elements to this form to insert or update a group
*
* @param array $formData The data sent by the user
*/
protected function createInsertElements(array $formData)
{
$this->addElement(
'text',
'group_name',
array(
'required' => true,
'label' => $this->translate('Group Name')
)
);
if ($this->shouldInsert()) {
$this->setTitle($this->translate('Add a new group'));
$this->setSubmitLabel($this->translate('Add'));
} else { // $this->shouldUpdate()
$this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier()));
$this->setSubmitLabel($this->translate('Save'));
}
}
/**
* Update a group
*
* @return bool
*/
protected function onUpdateSuccess()
{
if (parent::onUpdateSuccess()) {
if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) {
$this->getRedirectUrl()->setParam('group', $newName);
}
return true;
}
return false;
}
/**
* Create and add elements to this form to delete a group
*
* @param array $formData The data sent by the user
*/
protected function createDeleteElements(array $formData)
{
$this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier()));
$this->addDescription($this->translate(
'Note that all users that are currently a member of this group will'
. ' have their membership cleared automatically.'
));
$this->setSubmitLabel($this->translate('Yes'));
}
/**
* Create and return a filter to use when updating or deleting a group
*
* @return Filter
*/
protected function createFilter()
{
return Filter::where('group_name', $this->getIdentifier());
}
/**
* Return a notification message to use when inserting a group
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
protected function getInsertMessage($success)
{
if ($success) {
return $this->translate('Group added successfully');
} else {
return $this->translate('Failed to add group');
}
}
/**
* Return a notification message to use when updating a group
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
protected function getUpdateMessage($success)
{
if ($success) {
return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier());
} else {
return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier());
}
}
/**
* Return a notification message to use when deleting a group
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
protected function getDeleteMessage($success)
{
if ($success) {
return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier());
} else {
return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier());
}
}
}

View File

@ -0,0 +1,389 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Forms;
use Exception;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotFoundError;
use Icinga\Repository\Repository;
use Icinga\Web\Form;
use Icinga\Web\Notification;
/**
* Form base-class providing standard functionality for extensible, updatable and reducible repositories
*/
abstract class RepositoryForm extends Form
{
/**
* Insert mode
*/
const MODE_INSERT = 0;
/**
* Update mode
*/
const MODE_UPDATE = 1;
/**
* Delete mode
*/
const MODE_DELETE = 2;
/**
* The repository being worked with
*
* @var Repository
*/
protected $repository;
/**
* How to interact with the repository
*
* @var int
*/
protected $mode;
/**
* The name of the entry being handled when in mode update or delete
*
* @var string
*/
protected $identifier;
/**
* The data of the entry to pre-populate the form with when in mode insert or update
*
* @var type
*/
protected $data;
/**
* Set the repository to work with
*
* @param Repository $repository
*
* @return $this
*/
public function setRepository(Repository $repository)
{
$this->repository = $repository;
return $this;
}
/**
* Return the name of the entry to handle
*
* @return string
*/
protected function getIdentifier()
{
return $this->identifier;
}
/**
* Return the current data of the entry being handled
*
* @return array
*/
protected function getData()
{
return $this->data;
}
/**
* Return whether an entry should be inserted
*
* @return bool
*/
public function shouldInsert()
{
return $this->mode === self::MODE_INSERT;
}
/**
* Return whether an entry should be udpated
*
* @return bool
*/
public function shouldUpdate()
{
return $this->mode === self::MODE_UPDATE;
}
/**
* Return whether an entry should be deleted
*
* @return bool
*/
public function shouldDelete()
{
return $this->mode === self::MODE_DELETE;
}
/**
* Add a new entry
*
* @param array $data The defaults to use, if any
*
* @return $this
*/
public function add(array $data = array())
{
$this->mode = static::MODE_INSERT;
$this->data = $data;
return $this;
}
/**
* Edit an entry
*
* @param string $name The entry's name
* @param array $data The entry's current data
*
* @return $this
*/
public function edit($name, array $data = array())
{
$this->mode = static::MODE_UPDATE;
$this->identifier = $name;
$this->data = $data;
return $this;
}
/**
* Remove an entry
*
* @param string $name The entry's name
*
* @return $this
*/
public function remove($name)
{
$this->mode = static::MODE_DELETE;
$this->identifier = $name;
return $this;
}
/**
* Create and add elements to this form
*
* @param array $formData The data sent by the user
*/
public function createElements(array $formData)
{
if ($this->shouldInsert()) {
$this->createInsertElements($formData);
} elseif ($this->shouldUpdate()) {
$this->createUpdateElements($formData);
} elseif ($this->shouldDelete()) {
$this->createDeleteElements($formData);
}
}
/**
* Prepare the form for the requested mode
*/
public function onRequest()
{
if ($this->shouldInsert()) {
$this->onInsertRequest();
} elseif ($this->shouldUpdate()) {
$this->onUpdateRequest();
} elseif ($this->shouldDelete()) {
$this->onDeleteRequest();
}
}
/**
* Prepare the form for mode insert
*
* Populates the form with the data passed to add().
*/
protected function onInsertRequest()
{
$data = $this->getData();
if (! empty($data)) {
$this->populate($data);
}
}
/**
* Prepare the form for mode update
*
* Populates the form with either the data passed to edit() or tries to fetch it from the repository.
*
* @throws NotFoundError In case the entry to update cannot be found
*/
protected function onUpdateRequest()
{
$data = $this->getData();
if (empty($data)) {
$row = $this->repository->select()->applyFilter($this->createFilter())->fetchRow();
if ($row === false) {
throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
}
$data = get_object_vars($row);
}
$this->populate($data);
}
/**
* Prepare the form for mode delete
*
* Verifies that the repository contains the entry to delete.
*
* @throws NotFoundError In case the entry to delete cannot be found
*/
protected function onDeleteRequest()
{
if ($this->repository->select()->addFilter($this->createFilter())->count() === 0) {
throw new NotFoundError('Entry "%s" not found', $this->getIdentifier());
}
}
/**
* Apply the requested mode on the repository
*
* @return bool
*/
public function onSuccess()
{
if ($this->shouldInsert()) {
return $this->onInsertSuccess();
} elseif ($this->shouldUpdate()) {
return $this->onUpdateSuccess();
} elseif ($this->shouldDelete()) {
return $this->onDeleteSuccess();
}
}
/**
* Apply mode insert on the repository
*
* @return bool
*/
protected function onInsertSuccess()
{
try {
$this->repository->insert(
$this->repository->getBaseTable(),
$this->getValues()
);
} catch (Exception $e) {
Notification::error($this->getInsertMessage(false));
$this->error($e->getMessage());
return false;
}
Notification::success($this->getInsertMessage(true));
return true;
}
/**
* Apply mode update on the repository
*
* @return bool
*/
protected function onUpdateSuccess()
{
try {
$this->repository->update(
$this->repository->getBaseTable(),
$this->getValues(),
$this->createFilter()
);
} catch (Exception $e) {
Notification::error($this->getUpdateMessage(false));
$this->error($e->getMessage());
return false;
}
Notification::success($this->getUpdateMessage(true));
return true;
}
/**
* Apply mode delete on the repository
*
* @return bool
*/
protected function onDeleteSuccess()
{
try {
$this->repository->delete(
$this->repository->getBaseTable(),
$this->createFilter()
);
} catch (Exception $e) {
Notification::error($this->getDeleteMessage(false));
$this->error($e->getMessage());
return false;
}
Notification::success($this->getDeleteMessage(true));
return true;
}
/**
* Create and add elements to this form to insert an entry
*
* @param array $formData The data sent by the user
*/
abstract protected function createInsertElements(array $formData);
/**
* Create and add elements to this form to update an entry
*
* Calls createInsertElements() by default. Overwrite this to add different elements when in mode update.
*
* @param array $formData The data sent by the user
*/
protected function createUpdateElements(array $formData)
{
$this->createInsertElements($formData);
}
/**
* Create and add elements to this form to delete an entry
*
* @param array $formData The data sent by the user
*/
abstract protected function createDeleteElements(array $formData);
/**
* Create and return a filter to use when selecting, updating or deleting an entry
*
* @return Filter
*/
abstract protected function createFilter();
/**
* Return a notification message to use when inserting an entry
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
abstract protected function getInsertMessage($success);
/**
* Return a notification message to use when updating an entry
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
abstract protected function getUpdateMessage($success);
/**
* Return a notification message to use when deleting an entry
*
* @param bool $success true or false, whether the operation was successful
*
* @return string
*/
abstract protected function getDeleteMessage($success);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,78 @@
<?php
use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<div>
<?= $this->backendSelection; ?>
<?= $this->filterEditor; ?>
</div>
</div>
<?php endif ?>
<div class="content groups">
<?php
if ($backend === null) {
echo $this->translate('No backend found which is able to list groups') . '</div>';
return;
} else {
$extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
$reducible = $this->hasPermission('config/authentication/groups/remove') && $backend instanceof Reducible;
}
if (count($groups) > 0): ?>
<table data-base-target="_next" class="action group-list">
<thead>
<tr>
<th class="group-name"><?= $this->translate('Group'); ?></th>
<?php if ($reducible): ?>
<th class="group-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td class="group-name"><?= $this->qlink($group->group_name, 'group/show', array(
'backend' => $backend->getName(),
'group' => $group->group_name
), array(
'title' => sprintf($this->translate('Show detailed information for group %s'), $group->group_name)
)); ?></td>
<?php if ($reducible): ?>
<td class="group-remove">
<?= $this->qlink(
null,
'group/remove',
array(
'backend' => $backend->getName(),
'group' => $group->group_name
),
array(
'title' => sprintf($this->translate('Remove group %s'), $group->group_name),
'icon' => 'trash'
)
); ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No groups found matching the filter'); ?></p>
<?php endif ?>
<?php if ($extensible): ?>
<?= $this->qlink($this->translate('Add a new group'), 'group/add', array('backend' => $backend->getName()), array(
'icon' => 'plus',
'data-base-target' => '_next',
'class' => 'group-add'
)); ?>
<?php endif ?>
</div>

View File

@ -0,0 +1,81 @@
<?php
use Icinga\Data\Extensible;
use Icinga\Data\Updatable;
$extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
$editLink = null;
if ($this->hasPermission('config/authentication/groups/edit') && $backend instanceof Updatable) {
$editLink = $this->qlink(
null,
'group/edit',
array(
'backend' => $backend->getName(),
'group' => $group->group_name
),
array(
'title' => sprintf($this->translate('Edit group %s'), $group->group_name),
'class' => 'group-edit',
'icon' => 'edit'
)
);
}
?>
<div class="controls">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?php endif ?>
<div class="group-header">
<p class="group-name"><strong><?= $this->escape($group->group_name); ?></strong></p> <?= $editLink; ?>
<p class="group-created"><strong><?= $this->translate('Created at'); ?>:</strong> <?= $group->created_at === null ? '-' : $this->formatDateTime($group->created_at); ?></p>
<p class="group-modified"><strong><?= $this->translate('Last modified'); ?>:</strong> <?= $group->last_modified === null ? '-' : $this->formatDateTime($group->last_modified); ?></p>
</div>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content members" data-base-target="_next">
<?php if (count($members) > 0): ?>
<table data-base-target="_next" class="action member-list">
<thead>
<tr>
<th class="member-name"><?= $this->translate('Username'); ?></th>
<?php if (isset($removeForm)): ?>
<th class="member-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($members as $member): ?>
<tr>
<td class="member-name"><?= $this->escape($member->user_name); ?></td>
<?php if (isset($removeForm)): ?>
<td class="member-remove" data-base-target="_self">
<?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No group member found matching the filter'); ?></p>
<?php endif ?>
<?php if ($extensible): ?>
<?= $this->qlink($this->translate('Add a new member'), 'group/addmember', array(
'backend' => $backend->getName(),
'group' => $group->group_name
), array(
'icon' => 'plus',
'data-base-target' => '_next',
'class' => 'member-add'
)); ?>
<?php endif ?>
</div>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,78 @@
<?php
use Icinga\Data\Extensible;
use Icinga\Data\Reducible;
if (! $this->compact): ?>
<div class="controls">
<?= $this->tabs; ?>
<?= $this->sortBox; ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<div>
<?= $this->backendSelection; ?>
<?= $this->filterEditor; ?>
</div>
</div>
<?php endif ?>
<div class="content users">
<?php
if ($backend === null) {
echo $this->translate('No backend found which is able to list users') . '</div>';
return;
} else {
$extensible = $this->hasPermission('config/authentication/users/add') && $backend instanceof Extensible;
$reducible = $this->hasPermission('config/authentication/users/remove') && $backend instanceof Reducible;
}
if (count($users) > 0): ?>
<table data-base-target="_next" class="action user-list">
<thead>
<tr>
<th class="user-name"><?= $this->translate('Username'); ?></th>
<?php if ($reducible): ?>
<th class="user-remove"><?= $this->translate('Remove'); ?></th>
<?php endif ?>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td class="user-name"><?= $this->qlink($user->user_name, 'user/show', array(
'backend' => $backend->getName(),
'user' => $user->user_name
), array(
'title' => sprintf($this->translate('Show detailed information about %s'), $user->user_name)
)); ?></td>
<?php if ($reducible): ?>
<td class="user-remove">
<?= $this->qlink(
null,
'user/remove',
array(
'backend' => $backend->getName(),
'user' => $user->user_name
),
array(
'title' => sprintf($this->translate('Remove user %s'), $user->user_name),
'icon' => 'trash'
)
); ?>
</td>
<?php endif ?>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No users found matching the filter'); ?></p>
<?php endif ?>
<?php if ($extensible): ?>
<?= $this->qlink($this->translate('Add a new user'), 'user/add', array('backend' => $backend->getName()), array(
'icon' => 'plus',
'data-base-target' => '_next',
'class' => 'user-add'
)); ?>
<?php endif ?>
</div>

View File

@ -0,0 +1,95 @@
<?php
use Icinga\Data\Updatable;
use Icinga\Data\Reducible;
use Icinga\Data\Selectable;
$editLink = null;
if ($this->hasPermission('config/authentication/users/edit') && $backend instanceof Updatable) {
$editLink = $this->qlink(
null,
'user/edit',
array(
'backend' => $backend->getName(),
'user' => $user->user_name
),
array(
'title' => sprintf($this->translate('Edit user %s'), $user->user_name),
'class' => 'user-edit',
'icon' => 'edit'
)
);
}
?>
<div class="controls">
<?php if (! $this->compact): ?>
<?= $tabs; ?>
<?php endif ?>
<div class="user-header">
<p class="user-name"><strong><?= $this->escape($user->user_name); ?></strong></p> <?= $editLink; ?>
<p class="user-state"><strong><?= $this->translate('State'); ?>:</strong> <?= $user->is_active === null ? '-' : ($user->is_active ? $this->translate('Active') : $this->translate('Inactive')); ?></p>
<p class="user-created"><strong><?= $this->translate('Created at'); ?>:</strong> <?= $user->created_at === null ? '-' : $this->formatDateTime($user->created_at); ?></p>
<p class="user-modified"><strong><?= $this->translate('Last modified'); ?>:</strong> <?= $user->last_modified === null ? '-' : $this->formatDateTime($user->last_modified); ?></p>
</div>
<?php if (! $this->compact): ?>
<?= $this->sortBox; ?>
<?php endif ?>
<?= $this->limiter; ?>
<?= $this->paginator; ?>
<?php if (! $this->compact): ?>
<?= $this->filterEditor; ?>
<?php endif ?>
</div>
<div class="content memberships" data-base-target="_next">
<?php if (count($memberships) > 0): ?>
<table data-base-target="_next" class="action membership-list">
<thead>
<tr>
<th class="membership-group"><?= $this->translate('Group'); ?></th>
<th class="membership-cancel"><?= $this->translate('Cancel', 'group.membership'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($memberships as $membership): ?>
<tr>
<td class="membership-group">
<?php if ($this->hasPermission('config/authentication/groups/show') && $membership->backend instanceof Selectable): ?>
<?= $this->qlink($membership->group_name, 'group/show', array(
'backend' => $membership->backend->getName(),
'group' => $membership->group_name
), array(
'title' => sprintf($this->translate('Show detailed information for group %s'), $membership->group_name)
)); ?>
<?php else: ?>
<?= $this->escape($membership->group_name); ?>
<?php endif ?>
</td>
<td class="membership-cancel" data-base-target="_self">
<?php if (isset($removeForm) && $membership->backend instanceof Reducible): ?>
<?= $removeForm->setAction($this->url('group/removemember', array(
'backend' => $membership->backend->getName(),
'group' => $membership->group_name
))); ?>
<?php else: ?>
-
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<?php else: ?>
<p><?= $this->translate('No memberships found matching the filter'); ?></p>
<?php endif ?>
<?php if ($showCreateMembershipLink): ?>
<?= $this->qlink($this->translate('Create new membership'), 'user/createmembership', array(
'backend' => $backend->getName(),
'user' => $user->user_name
), array(
'icon' => 'plus',
'data-base-target' => '_next',
'class' => 'membership-create'
)); ?>
<?php endif ?>
</div>

View File

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

View File

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

View File

@ -1,21 +1,25 @@
# Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+
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`(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,206 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use PDO;
use Icinga\Authentication\UserBackend;
use Icinga\Data\Db\DbConnection;
use Icinga\User;
use Icinga\Exception\AuthenticationException;
use Exception;
use Zend_Db_Expr;
use Zend_Db_Select;
class DbUserBackend extends UserBackend
{
/**
* The algorithm to use when hashing passwords
*
* @var string
*/
const HASH_ALGORITHM = '$1$'; // MD5
/**
* The length of the salt to use when hashing a password
*
* @var int
*/
const SALT_LENGTH = 12; // 12 is required by MD5
/**
* Connection to the database
*
* @var DbConnection
*/
protected $conn;
public function __construct(DbConnection $conn)
{
$this->conn = $conn;
}
/**
* Test whether the given user exists
*
* @param User $user
*
* @return bool
*/
public function hasUser(User $user)
{
$select = new Zend_Db_Select($this->conn->getDbAdapter());
$row = $select->from('icingaweb_user', array(new Zend_Db_Expr(1)))
->where('name = ?', $user->getUsername())
->query()->fetchObject();
return ($row !== false) ? true : false;
}
/**
* Add a new user
*
* @param string $username The name of the new user
* @param string $password The new user's password
* @param bool $active Whether the user is active
*/
public function addUser($username, $password, $active = true)
{
$passwordHash = $this->hashPassword($password);
$stmt = $this->conn->getDbAdapter()->prepare(
'INSERT INTO icingaweb_user VALUES (:name, :active, :password_hash, now(), DEFAULT);'
);
$stmt->bindParam(':name', $username, PDO::PARAM_STR);
$stmt->bindParam(':active', $active, PDO::PARAM_INT);
$stmt->bindParam(':password_hash', $passwordHash, PDO::PARAM_LOB);
$stmt->execute();
}
/**
* Fetch the hashed password for the given user
*
* @param string $username The name of the user
*
* @return string
*/
protected function getPasswordHash($username)
{
if ($this->conn->getDbType() === 'pgsql') {
// Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
$stmt = $this->conn->getDbAdapter()->prepare(
'SELECT ENCODE(password_hash, \'escape\') FROM icingaweb_user WHERE name = :name AND active = 1'
);
} else {
$stmt = $this->conn->getDbAdapter()->prepare(
'SELECT password_hash FROM icingaweb_user WHERE name = :name AND active = 1'
);
}
$stmt->execute(array(':name' => $username));
$stmt->bindColumn(1, $lob, PDO::PARAM_LOB);
$stmt->fetch(PDO::FETCH_BOUND);
if (is_resource($lob)) {
$lob = stream_get_contents($lob);
}
return $this->conn->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob;
}
/**
* Authenticate the given user and return true on success, false on failure and throw an exception on error
*
* @param User $user
* @param string $password
*
* @return bool
*
* @throws AuthenticationException
*/
public function authenticate(User $user, $password)
{
try {
$passwordHash = $this->getPasswordHash($user->getUsername());
$passwordSalt = $this->getSalt($passwordHash);
$hashToCompare = $this->hashPassword($password, $passwordSalt);
return $hashToCompare === $passwordHash;
} catch (Exception $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
$user->getUsername(),
$this->getName(),
$e
);
}
}
/**
* Extract salt from the given password hash
*
* @param string $hash The hashed password
*
* @return string
*/
protected function getSalt($hash)
{
return substr($hash, strlen(self::HASH_ALGORITHM), self::SALT_LENGTH);
}
/**
* Return a random salt
*
* The returned salt is safe to be used for hashing a user's password
*
* @return string
*/
protected function generateSalt()
{
return openssl_random_pseudo_bytes(self::SALT_LENGTH);
}
/**
* Hash a password
*
* @param string $password
* @param string $salt
*
* @return string
*/
protected function hashPassword($password, $salt = null)
{
return crypt($password, self::HASH_ALGORITHM . ($salt !== null ? $salt : $this->generateSalt()));
}
/**
* Get the number of users available
*
* @return int
*/
public function count()
{
$select = new Zend_Db_Select($this->conn->getDbAdapter());
$row = $select->from(
'icingaweb_user',
array('count' => 'COUNT(*)')
)->query()->fetchObject();
return ($row !== false) ? $row->count : 0;
}
/**
* Return the names of all available users
*
* @return array
*/
public function listUsers()
{
$query = $this->conn->select()->from('icingaweb_user', array('name'));
$users = array();
foreach ($query->fetchAll() as $row) {
$users[] = $row->name;
}
return $users;
}
}

View File

@ -1,62 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use Icinga\Authentication\UserGroupBackend;
use Icinga\Data\Db\DbConnection;
use Icinga\User;
/**
* Database user group backend
*/
class DbUserGroupBackend extends UserGroupBackend
{
/**
* Connection to the database
*
* @var DbConnection
*/
private $conn;
/**
* Create a new database user group backend
*
* @param DbConnection $conn
*/
public function __construct(DbConnection $conn)
{
$this->conn = $conn;
}
/**
* (non-PHPDoc)
* @see UserGroupBackend::getMemberships() For the method documentation.
*/
public function getMemberships(User $user)
{
$groups = array();
$groupsStmt = $this->conn->getDbAdapter()
->select()
->from($this->conn->getTablePrefix() . 'group', array('name', 'parent'))
->query();
foreach ($groupsStmt as $group) {
$groups[$group->name] = $group->parent;
}
$memberships = array();
$membershipsStmt = $this->conn->getDbAdapter()
->select()
->from($this->conn->getTablePrefix() . 'group_membership', array('group_name'))
->where('username = ?', $user->getUsername())
->query();
foreach ($membershipsStmt as $membership) {
$memberships[] = $membership->group_name;
$parent = $groups[$membership->group_name];
while (isset($parent)) {
$memberships[] = $parent;
$parent = $groups[$parent];
}
}
return $memberships;
}
}

View File

@ -1,64 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use Icinga\Application\Config;
use Icinga\Authentication\UserGroupBackend;
use Icinga\Exception\ConfigurationError;
use Icinga\User;
use Icinga\Util\String;
/**
* INI user group backend
*/
class IniUserGroupBackend extends UserGroupBackend
{
/**
* Config
*
* @var Config
*/
private $config;
/**
* Create a new INI user group backend
*
* @param Config $config
*/
public function __construct(Config $config)
{
$this->config = $config;
}
/**
* (non-PHPDoc)
* @see UserGroupBackend::getMemberships() For the method documentation.
*/
public function getMemberships(User $user)
{
$username = strtolower($user->getUsername());
$groups = array();
foreach ($this->config as $name => $section) {
if (empty($section->users)) {
throw new ConfigurationError(
'Membership section \'%s\' in \'%s\' is missing the \'users\' section',
$name,
$this->config->getConfigFile()
);
}
if (empty($section->groups)) {
throw new ConfigurationError(
'Membership section \'%s\' in \'%s\' is missing the \'groups\' section',
$name,
$this->config->getConfigFile()
);
}
$users = array_map('strtolower', String::trimSplit($section->users));
if (in_array($username, $users)) {
$groups = array_merge($groups, array_diff(String::trimSplit($section->groups), $groups));
}
}
return $groups;
}
}

View File

@ -1,291 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\Backend;
use Icinga\User;
use Icinga\Authentication\UserBackend;
use Icinga\Protocol\Ldap\Query;
use Icinga\Protocol\Ldap\Connection;
use Icinga\Exception\AuthenticationException;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression;
class LdapUserBackend extends UserBackend
{
/**
* Connection to the LDAP server
*
* @var Connection
*/
protected $conn;
protected $baseDn;
protected $userClass;
protected $userNameAttribute;
protected $customFilter;
protected $groupOptions;
/**
* Normed attribute names based on known LDAP environments
*
* @var array
*/
protected $normedAttributes = array(
'uid' => 'uid',
'user' => 'user',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName'
);
public function __construct(
Connection $conn,
$userClass,
$userNameAttribute,
$baseDn,
$cutomFilter,
$groupOptions = null
) {
$this->conn = $conn;
$this->baseDn = trim($baseDn) ?: $conn->getDN();
$this->userClass = $this->getNormedAttribute($userClass);
$this->userNameAttribute = $this->getNormedAttribute($userNameAttribute);
$this->customFilter = trim($cutomFilter);
$this->groupOptions = $groupOptions;
}
/**
* Return the given attribute name normed to known LDAP enviroments, if possible
*
* @param string $name
*
* @return string
*/
protected function getNormedAttribute($name)
{
$loweredName = strtolower($name);
if (array_key_exists($loweredName, $this->normedAttributes)) {
return $this->normedAttributes[$loweredName];
}
return $name;
}
/**
* Create a query to select all usernames
*
* @return Query
*/
protected function selectUsers()
{
$query = $this->conn->select()->setBase($this->baseDn)->from(
$this->userClass,
array(
$this->userNameAttribute
)
);
if ($this->customFilter) {
$query->addFilter(new Expression($this->customFilter));
}
return $query;
}
/**
* Create a query filtered by the given username
*
* @param string $username
*
* @return Query
*/
protected function selectUser($username)
{
return $this->selectUsers()->setUsePagedResults(false)->where(
$this->userNameAttribute,
str_replace('*', '', $username)
);
}
/**
* Probe the backend to test if authentication is possible
*
* Try to bind to the backend and query all available users to check if:
* <ul>
* <li>Connection credentials are correct and the bind is possible</li>
* <li>At least one user exists</li>
* <li>The specified userClass has the property specified by userNameAttribute</li>
* </ul>
*
* @throws AuthenticationException When authentication is not possible
*/
public function assertAuthenticationPossible()
{
try {
$result = $this->selectUsers()->fetchRow();
} catch (LdapException $e) {
throw new AuthenticationException('Connection not possible.', $e);
}
if ($result === null) {
throw new AuthenticationException(
'No objects with objectClass="%s" in DN="%s" found. (Filter: %s)',
$this->userClass,
$this->baseDn,
$this->customFilter ?: 'None'
);
}
if (! isset($result->{$this->userNameAttribute})) {
throw new AuthenticationException(
'UserNameAttribute "%s" not existing in objectClass="%s"',
$this->userNameAttribute,
$this->userClass
);
}
}
/**
* Retrieve the user groups
*
* @TODO: Subject to change, see #7343
*
* @param string $dn
*
* @return array
*/
public function getGroups($dn)
{
if (empty($this->groupOptions) || ! isset($this->groupOptions['group_base_dn'])) {
return array();
}
$q = $this->conn->select()
->setBase($this->groupOptions['group_base_dn'])
->from(
$this->groupOptions['group_class'],
array($this->groupOptions['group_attribute'])
)
->where(
$this->groupOptions['group_member_attribute'],
$dn
);
$result = $this->conn->fetchAll($q);
$groups = array();
foreach ($result as $group) {
$groups[] = $group->{$this->groupOptions['group_attribute']};
}
return $groups;
}
/**
* Return whether the given user exists
*
* @param User $user
*
* @return bool
*/
public function hasUser(User $user)
{
$username = $user->getUsername();
$entry = $this->selectUser($username)->fetchOne();
if (is_array($entry)) {
return in_array(strtolower($username), array_map('strtolower', $entry));
}
return strtolower($entry) === strtolower($username);
}
/**
* Return whether the given user credentials are valid
*
* @param User $user
* @param string $password
* @param boolean $healthCheck Assert that authentication is possible at all
*
* @return bool
*
* @throws AuthenticationException In case an error occured or the health check has failed
*/
public function authenticate(User $user, $password, $healthCheck = false)
{
if ($healthCheck) {
try {
$this->assertAuthenticationPossible();
} catch (AuthenticationException $e) {
throw new AuthenticationException(
'Authentication against backend "%s" not possible.',
$this->getName(),
$e
);
}
}
if (! $this->hasUser($user)) {
return false;
}
try {
$userDn = $this->conn->fetchDN($this->selectUser($user->getUsername()));
$authenticated = $this->conn->testCredentials(
$userDn,
$password
);
if ($authenticated) {
$groups = $this->getGroups($userDn);
if ($groups !== null) {
$user->setGroups($groups);
}
}
return $authenticated;
} catch (LdapException $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
$user->getUsername(),
$this->getName(),
$e
);
}
}
/**
* Get the number of users available
*
* @return int
*/
public function count()
{
return $this->selectUsers()->count();
}
/**
* Return the names of all available users
*
* @return array
*/
public function listUsers()
{
$users = array();
foreach ($this->selectUsers()->fetchAll() as $row) {
if (is_array($row->{$this->userNameAttribute})) {
foreach ($row->{$this->userNameAttribute} as $col) {
$users[] = $col;
}
} else {
$users[] = $row->{$this->userNameAttribute};
}
}
return $users;
}
}

View File

@ -4,6 +4,7 @@
namespace Icinga\Authentication;
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

View File

@ -0,0 +1,249 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use Exception;
use PDO;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\AuthenticationException;
use Icinga\Repository\DbRepository;
use Icinga\User;
class DbUserBackend extends DbRepository implements UserBackendInterface
{
/**
* The algorithm to use when hashing passwords
*
* @var string
*/
const HASH_ALGORITHM = '$1$'; // MD5
/**
* The length of the salt to use when hashing a password
*
* @var int
*/
const SALT_LENGTH = 12; // 12 is required by MD5
/**
* The query columns being provided
*
* @var array
*/
protected $queryColumns = array(
'user' => array(
'user' => 'name COLLATE utf8_general_ci',
'user_name' => 'name',
'is_active' => 'active',
'created_at' => 'UNIX_TIMESTAMP(ctime)',
'last_modified' => 'UNIX_TIMESTAMP(mtime)'
)
);
/**
* The statement columns being provided
*
* @var array
*/
protected $statementColumns = array(
'user' => array(
'password' => 'password_hash',
'created_at' => 'ctime',
'last_modified' => 'mtime'
)
);
/**
* The columns which are not permitted to be queried
*
* @var array
*/
protected $filterColumns = array('user');
/**
* The default sort rules to be applied on a query
*
* @var array
*/
protected $sortRules = array(
'user_name' => array(
'columns' => array(
'is_active desc',
'user_name'
)
)
);
/**
* The value conversion rules to apply on a query or statement
*
* @var array
*/
protected $conversionRules = array(
'user' => array(
'password'
)
);
/**
* Initialize this database user backend
*/
protected function init()
{
if (! $this->ds->getTablePrefix()) {
$this->ds->setTablePrefix('icingaweb_');
}
}
/**
* Insert a table row with the given data
*
* @param string $table
* @param array $bind
*/
public function insert($table, array $bind)
{
$bind['created_at'] = date('Y-m-d H:i:s');
$this->ds->insert(
$this->prependTablePrefix($table),
$this->requireStatementColumns($table, $bind),
array(
'active' => PDO::PARAM_INT,
'password_hash' => PDO::PARAM_LOB
)
);
}
/**
* Update table rows with the given data, optionally limited by using a filter
*
* @param string $table
* @param array $bind
* @param Filter $filter
*/
public function update($table, array $bind, Filter $filter = null)
{
$bind['last_modified'] = date('Y-m-d H:i:s');
if ($filter) {
$filter = $this->requireFilter($table, $filter);
}
$this->ds->update(
$this->prependTablePrefix($table),
$this->requireStatementColumns($table, $bind),
$filter,
array(
'active' => PDO::PARAM_INT,
'password_hash' => PDO::PARAM_LOB
)
);
}
/**
* Hash and return the given password
*
* @param string $value
*
* @return string
*/
protected function persistPassword($value)
{
return $this->hashPassword($value);
}
/**
* Fetch the hashed password for the given user
*
* @param string $username The name of the user
*
* @return string
*/
protected function getPasswordHash($username)
{
if ($this->ds->getDbType() === 'pgsql') {
// Since PostgreSQL version 9.0 the default value for bytea_output is 'hex' instead of 'escape'
$columns = array('password_hash' => 'ENCODE(password_hash, \'escape\')');
} else {
$columns = array('password_hash');
}
$query = $this->ds->select()
->from($this->prependTablePrefix('user'), $columns)
->where('name', $username)
->where('active', true);
$statement = $this->ds->getDbAdapter()->prepare($query->getSelectQuery());
$statement->execute();
$statement->bindColumn(1, $lob, PDO::PARAM_LOB);
$statement->fetch(PDO::FETCH_BOUND);
if (is_resource($lob)) {
$lob = stream_get_contents($lob);
}
return $this->ds->getDbType() === 'pgsql' ? pg_unescape_bytea($lob) : $lob;
}
/**
* Authenticate the given user
*
* @param User $user
* @param string $password
*
* @return bool True on success, false on failure
*
* @throws AuthenticationException In case authentication is not possible due to an error
*/
public function authenticate(User $user, $password)
{
try {
$passwordHash = $this->getPasswordHash($user->getUsername());
$passwordSalt = $this->getSalt($passwordHash);
$hashToCompare = $this->hashPassword($password, $passwordSalt);
return $hashToCompare === $passwordHash;
} catch (Exception $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
$user->getUsername(),
$this->getName(),
$e
);
}
}
/**
* Extract salt from the given password hash
*
* @param string $hash The hashed password
*
* @return string
*/
protected function getSalt($hash)
{
return substr($hash, strlen(self::HASH_ALGORITHM), self::SALT_LENGTH);
}
/**
* Return a random salt
*
* The returned salt is safe to be used for hashing a user's password
*
* @return string
*/
protected function generateSalt()
{
return openssl_random_pseudo_bytes(self::SALT_LENGTH);
}
/**
* Hash a password
*
* @param string $password
* @param string $salt
*
* @return string
*/
protected function hashPassword($password, $salt = null)
{
return crypt($password, self::HASH_ALGORITHM . ($salt !== null ? $salt : $this->generateSalt()));
}
}

View File

@ -1,23 +1,29 @@
<?php
/* 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);
}
}

View File

@ -0,0 +1,493 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use DateTime;
use Icinga\Application\Logger;
use Icinga\Data\ConfigObject;
use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ProgrammingError;
use Icinga\Repository\Repository;
use Icinga\Repository\RepositoryQuery;
use Icinga\Protocol\Ldap\Exception as LdapException;
use Icinga\Protocol\Ldap\Expression;
use Icinga\User;
class LdapUserBackend extends Repository implements UserBackendInterface
{
/**
* The base DN to use for a query
*
* @var string
*/
protected $baseDn;
/**
* The objectClass where look for users
*
* @var string
*/
protected $userClass;
/**
* The attribute name where to find a user's name
*
* @var string
*/
protected $userNameAttribute;
/**
* The custom LDAP filter to apply on search queries
*
* @var string
*/
protected $filter;
/**
* The columns which are not permitted to be queried
*
* @var array
*/
protected $filterColumns = array('user');
/**
* The default sort rules to be applied on a query
*
* @var array
*/
protected $sortRules = array(
'user_name' => array(
'columns' => array(
'is_active desc',
'user_name'
)
)
);
protected $groupOptions;
/**
* Normed attribute names based on known LDAP environments
*
* @var array
*/
protected $normedAttributes = array(
'uid' => 'uid',
'user' => 'user',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName'
);
/**
* Set the base DN to use for a query
*
* @param string $baseDn
*
* @return $this
*/
public function setBaseDn($baseDn)
{
if (($baseDn = trim($baseDn))) {
$this->baseDn = $baseDn;
}
return $this;
}
/**
* Return the base DN to use for a query
*
* @return string
*/
public function getBaseDn()
{
return $this->baseDn;
}
/**
* Set the objectClass where to look for users
*
* Sets also the base table name for the underlying repository.
*
* @param string $userClass
*
* @return $this
*/
public function setUserClass($userClass)
{
$this->baseTable = $this->userClass = $this->getNormedAttribute($userClass);
return $this;
}
/**
* Return the objectClass where to look for users
*
* @return string
*/
public function getUserClass()
{
return $this->userClass;
}
/**
* Set the attribute name where to find a user's name
*
* @param string $userNameAttribute
*
* @return $this
*/
public function setUserNameAttribute($userNameAttribute)
{
$this->userNameAttribute = $this->getNormedAttribute($userNameAttribute);
return $this;
}
/**
* Return the attribute name where to find a user's name
*
* @return string
*/
public function getUserNameAttribute()
{
return $this->userNameAttribute;
}
/**
* Set the custom LDAP filter to apply on search queries
*
* @param string $filter
*
* @return $this
*/
public function setFilter($filter)
{
if (($filter = trim($filter))) {
$this->filter = $filter;
}
return $this;
}
/**
* Return the custom LDAP filter to apply on search queries
*
* @return string
*/
public function getFilter()
{
return $this->filter;
}
public function setGroupOptions(array $options)
{
$this->groupOptions = $options;
return $this;
}
public function getGroupOptions()
{
return $this->groupOptions;
}
/**
* Return the given attribute name normed to known LDAP enviroments, if possible
*
* @param string $name
*
* @return string
*/
protected function getNormedAttribute($name)
{
$loweredName = strtolower($name);
if (array_key_exists($loweredName, $this->normedAttributes)) {
return $this->normedAttributes[$loweredName];
}
return $name;
}
/**
* Apply the given configuration to this backend
*
* @param ConfigObject $config
*
* @return $this
*/
public function setConfig(ConfigObject $config)
{
return $this
->setBaseDn($config->base_dn)
->setUserClass($config->user_class)
->setUserNameAttribute($config->user_name_attribute)
->setFilter($config->filter);
}
/**
* Return a new query for the given columns
*
* @param array $columns The desired columns, if null all columns will be queried
*
* @return RepositoryQuery
*/
public function select(array $columns = null)
{
$query = parent::select($columns);
$query->getQuery()->setBase($this->baseDn);
if ($this->filter) {
$query->getQuery()->where(new Expression($this->filter));
}
return $query;
}
/**
* Initialize this repository's query columns
*
* @return array
*
* @throws ProgrammingError In case either $this->userNameAttribute or $this->userClass has not been set yet
*/
protected function initializeQueryColumns()
{
if ($this->userClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for users first');
}
if ($this->userNameAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first');
}
if ($this->ds->getCapabilities()->hasAdOid()) {
$isActiveAttribute = 'userAccountControl';
$createdAtAttribute = 'whenCreated';
$lastModifiedAttribute = 'whenChanged';
} else {
// TODO(jom): Elaborate whether it is possible to add dynamic support for the ppolicy
$isActiveAttribute = 'shadowExpire';
$createdAtAttribute = 'createTimestamp';
$lastModifiedAttribute = 'modifyTimestamp';
}
return array(
$this->userClass => array(
'user' => $this->userNameAttribute,
'user_name' => $this->userNameAttribute,
'is_active' => $isActiveAttribute,
'created_at' => $createdAtAttribute,
'last_modified' => $lastModifiedAttribute
)
);
}
/**
* Initialize this repository's conversion rules
*
* @return array
*
* @throws ProgrammingError In case $this->userClass has not been set yet
*/
protected function initializeConversionRules()
{
if ($this->userClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for users first');
}
if ($this->ds->getCapabilities()->hasAdOid()) {
$stateConverter = 'user_account_control';
} else {
$stateConverter = 'shadow_expire';
}
return array(
$this->userClass => array(
'is_active' => $stateConverter,
'created_at' => 'generalized_time',
'last_modified' => 'generalized_time'
)
);
}
/**
* Return whether the given userAccountControl value defines that a user is permitted to login
*
* @param string|null $value
*
* @return bool
*/
protected function retrieveUserAccountControl($value)
{
if ($value === null) {
return $value;
}
$ADS_UF_ACCOUNTDISABLE = 2;
return ((int) $value & $ADS_UF_ACCOUNTDISABLE) === 0;
}
/**
* Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation
*
* @param string|null $value
*
* @return int
*/
protected function retrieveGeneralizedTime($value)
{
if ($value === null) {
return $value;
}
if (
($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false
) {
return $dateTime->getTimeStamp();
} else {
Logger::debug(sprintf(
'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) for user backend "%s".',
$value,
$this->getName()
));
}
}
/**
* Return whether the given shadowExpire value defines that a user is permitted to login
*
* @param string|null $value
*
* @return bool
*/
protected function retrieveShadowExpire($value)
{
if ($value === null) {
return $value;
}
$now = new DateTime();
$bigBang = clone $now;
$bigBang->setTimestamp(0);
return ((int) $value) >= $bigBang->diff($now)->days;
}
/**
* Probe the backend to test if authentication is possible
*
* Try to bind to the backend and fetch a single user to check if:
* <ul>
* <li>Connection credentials are correct and the bind is possible</li>
* <li>At least one user exists</li>
* <li>The specified userClass has the property specified by userNameAttribute</li>
* </ul>
*
* @throws AuthenticationException When authentication is not possible
*/
public function assertAuthenticationPossible()
{
try {
$result = $this->select()->fetchRow();
} catch (LdapException $e) {
throw new AuthenticationException('Connection not possible.', $e);
}
if ($result === null) {
throw new AuthenticationException(
'No objects with objectClass "%s" in DN "%s" found. (Filter: %s)',
$this->userClass,
$this->baseDn ?: $this->ds->getDn(),
$this->filter ?: 'None'
);
}
if (! isset($result->user_name)) {
throw new AuthenticationException(
'UserNameAttribute "%s" not existing in objectClass "%s"',
$this->userNameAttribute,
$this->userClass
);
}
}
/**
* Retrieve the user groups
*
* @TODO: Subject to change, see #7343
*
* @param string $dn
*
* @return array
*/
public function getGroups($dn)
{
if (empty($this->groupOptions) || ! isset($this->groupOptions['group_base_dn'])) {
return array();
}
$result = $this->ds->select()
->setBase($this->groupOptions['group_base_dn'])
->from(
$this->groupOptions['group_class'],
array($this->groupOptions['group_attribute'])
)
->where(
$this->groupOptions['group_member_attribute'],
$dn
)
->fetchAll();
$groups = array();
foreach ($result as $group) {
$groups[] = $group->{$this->groupOptions['group_attribute']};
}
return $groups;
}
/**
* Authenticate the given user
*
* @param User $user
* @param string $password
*
* @return bool True on success, false on failure
*
* @throws AuthenticationException In case authentication is not possible due to an error
*/
public function authenticate(User $user, $password)
{
try {
$userDn = $this
->select()
->where('user_name', str_replace('*', '', $user->getUsername()))
->getQuery()
->setUsePagedResults(false)
->fetchDn();
if ($userDn === null) {
return false;
}
$authenticated = $this->ds->testCredentials($userDn, $password);
if ($authenticated) {
$groups = $this->getGroups($userDn);
if ($groups !== null) {
$user->setGroups($groups);
}
}
return $authenticated;
} catch (LdapException $e) {
throw new AuthenticationException(
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
$user->getUsername(),
$this->getName(),
$e
);
}
}
}

View File

@ -0,0 +1,193 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use Icinga\Application\Logger;
use Icinga\Application\Icinga;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
/**
* Factory for user backends
*/
class UserBackend
{
/**
* The default user backend types provided by Icinga Web 2
*
* @var array
*/
protected static $defaultBackends = array(
'external',
'db',
'ldap',
'msldap'
);
/**
* The registered custom user backends with their identifier as key and class name as value
*
* @var array
*/
protected static $customBackends;
/**
* Register all custom user backends from all loaded modules
*/
protected static function registerCustomUserBackends()
{
if (static::$customBackends !== null) {
return;
}
static::$customBackends = array();
$providedBy = array();
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
foreach ($module->getUserBackends() as $identifier => $className) {
if (array_key_exists($identifier, $providedBy)) {
Logger::warning(
'Cannot register user backend of type "%s" provided by module "%s".'
. ' The type is already provided by module "%s"',
$identifier,
$module->getName(),
$providedBy[$identifier]
);
} elseif (in_array($identifier, static::$defaultBackends)) {
Logger::warning(
'Cannot register user backend of type "%s" provided by module "%s".'
. ' The type is a default type provided by Icinga Web 2',
$identifier,
$module->getName()
);
} else {
$providedBy[$identifier] = $module->getName();
static::$customBackends[$identifier] = $className;
}
}
}
}
/**
* Return the class for the given custom user backend
*
* @param string $identifier The identifier of the custom user backend
*
* @return string|null The name of the class or null in case there was no
* backend found with the given identifier
*
* @throws ConfigurationError In case the class associated to the given identifier does not exist
*/
protected static function getCustomUserBackend($identifier)
{
static::registerCustomUserBackends();
if (array_key_exists($identifier, static::$customBackends)) {
$className = static::$customBackends[$identifier];
if (! class_exists($className)) {
throw new ConfigurationError(
'Cannot utilize user backend of type "%s". Class "%s" does not exist',
$identifier,
$className
);
}
return $className;
}
}
/**
* Create and return a user backend with the given name and given configuration applied to it
*
* @param string $name
* @param ConfigObject $backendConfig
*
* @return UserBackendInterface
*
* @throws ConfigurationError
*/
public static function create($name, ConfigObject $backendConfig)
{
if ($backendConfig->name !== null) {
$name = $backendConfig->name;
}
if (! ($backendType = strtolower($backendConfig->backend))) {
throw new ConfigurationError(
'Authentication configuration for user backend "%s" is missing the \'backend\' directive',
$name
);
}
if ($backendType === 'external') {
$backend = new ExternalBackend($backendConfig);
$backend->setName($name);
return $backend;
}
if (in_array($backendType, static::$defaultBackends)) {
// The default backend check is the first one because of performance reasons:
// Do not attempt to load a custom user backend unless it's actually required
} elseif (($customClass = static::getCustomUserBackend($backendType)) !== null) {
$backend = new $customClass($backendConfig);
if (! is_a($backend, 'Icinga\Authentication\User\UserBackendInterface')) {
throw new ConfigurationError(
'Cannot utilize user backend of type "%s". Class "%s" does not implement UserBackendInterface',
$backendType,
$customClass
);
}
$backend->setName($name);
return $backend;
} else {
throw new ConfigurationError(
'Authentication configuration for user backend "%s" defines an invalid backend type.'
. ' Backend type "%s" is not supported',
$name,
$backendType
);
}
if ($backendConfig->resource === null) {
throw new ConfigurationError(
'Authentication configuration for user backend "%s" is missing the \'resource\' directive',
$name
);
}
$resource = ResourceFactory::create($backendConfig->resource);
switch ($backendType) {
case 'db':
$backend = new DbUserBackend($resource);
break;
case 'msldap':
$backend = new LdapUserBackend($resource);
$backend->setBaseDn($backendConfig->base_dn);
$backend->setUserClass($backendConfig->get('user_class', 'user'));
$backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'sAMAccountName'));
$backend->setFilter($backendConfig->filter);
$backend->setGroupOptions(array(
'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()),
'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'),
'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'),
'group_class' => $backendConfig->get('group_class', 'group')
));
break;
case 'ldap':
$backend = new LdapUserBackend($resource);
$backend->setBaseDn($backendConfig->base_dn);
$backend->setUserClass($backendConfig->get('user_class', 'inetOrgPerson'));
$backend->setUserNameAttribute($backendConfig->get('user_name_attribute', 'uid'));
$backend->setFilter($backendConfig->filter);
$backend->setGroupOptions(array(
'group_base_dn' => $backendConfig->group_base_dn,
'group_attribute' => $backendConfig->group_attribute,
'group_member_attribute' => $backendConfig->group_member_attribute,
'group_class' => $backendConfig->group_class
));
break;
}
$backend->setName($name);
return $backend;
}
}

View File

@ -0,0 +1,41 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\User;
use Icinga\Exception\AuthenticationException;
use Icinga\User;
/**
* Interface for user backends
*/
interface UserBackendInterface
{
/**
* Set this backend's name
*
* @param string $name
*
* @return $this
*/
public function setName($name);
/**
* Return this backend's name
*
* @return string
*/
public function getName();
/**
* Authenticate the given user
*
* @param User $user
* @param string $password
*
* @return bool True on success, false on failure
*
* @throws AuthenticationException In case authentication is not possible due to an error
*/
public function authenticate(User $user, $password);
}

View File

@ -1,168 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication;
use Countable;
use Icinga\Authentication\Backend\ExternalBackend;
use Icinga\Authentication\Backend\DbUserBackend;
use Icinga\Authentication\Backend\LdapUserBackend;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
use Icinga\User;
abstract class UserBackend implements Countable
{
/**
* Name of the backend
*
* @var string
*/
protected $name;
/**
* Setter for the backend's name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Getter for the backend's name
*
* @return string
*/
public function getName()
{
return $this->name;
}
public static function create($name, ConfigObject $backendConfig)
{
if ($backendConfig->name !== null) {
$name = $backendConfig->name;
}
if (isset($backendConfig->class)) {
// Use a custom backend class, this is only useful for testing
if (!class_exists($backendConfig->class)) {
throw new ConfigurationError(
'Authentication configuration for backend "%s" defines an invalid backend class.'
. ' Backend class "%s" not found',
$name,
$backendConfig->class
);
}
return new $backendConfig->class($backendConfig);
}
if (($backendType = $backendConfig->backend) === null) {
throw new ConfigurationError(
'Authentication configuration for backend "%s" is missing the backend directive',
$name
);
}
$backendType = strtolower($backendType);
if ($backendType === 'external') {
$backend = new ExternalBackend($backendConfig);
$backend->setName($name);
return $backend;
}
if ($backendConfig->resource === null) {
throw new ConfigurationError(
'Authentication configuration for backend "%s" is missing the resource directive',
$name
);
}
try {
$resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource);
} catch (ProgrammingError $e) {
throw new ConfigurationError(
'Resources not set up. Please contact your Icinga Web administrator'
);
}
$resource = ResourceFactory::createResource($resourceConfig);
switch ($backendType) {
case 'db':
$backend = new DbUserBackend($resource);
break;
case 'msldap':
$groupOptions = array(
'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()),
'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'),
'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'),
'group_class' => $backendConfig->get('group_class', 'group')
);
$backend = new LdapUserBackend(
$resource,
$backendConfig->get('user_class', 'user'),
$backendConfig->get('user_name_attribute', 'sAMAccountName'),
$backendConfig->get('base_dn', $resource->getDN()),
$backendConfig->get('filter'),
$groupOptions
);
break;
case 'ldap':
if ($backendConfig->user_class === null) {
throw new ConfigurationError(
'Authentication configuration for backend "%s" is missing the user_class directive',
$name
);
}
if ($backendConfig->user_name_attribute === null) {
throw new ConfigurationError(
'Authentication configuration for backend "%s" is missing the user_name_attribute directive',
$name
);
}
$groupOptions = array(
'group_base_dn' => $backendConfig->group_base_dn,
'group_attribute' => $backendConfig->group_attribute,
'group_member_attribute' => $backendConfig->group_member_attribute,
'group_class' => $backendConfig->group_class
);
$backend = new LdapUserBackend(
$resource,
$backendConfig->user_class,
$backendConfig->user_name_attribute,
$backendConfig->get('base_dn', $resource->getDN()),
$backendConfig->get('filter'),
$groupOptions
);
break;
default:
throw new ConfigurationError(
'Authentication configuration for backend "%s" defines an invalid backend type.'
. ' Backend type "%s" is not supported',
$name,
$backendType
);
}
$backend->setName($name);
return $backend;
}
/**
* Test whether the given user exists
*
* @param User $user
*
* @return bool
*/
abstract public function hasUser(User $user);
/**
* Authenticate
*
* @param User $user
* @param string $password
*
* @return bool
*/
abstract public function authenticate(User $user, $password);
}

View File

@ -0,0 +1,264 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\NotFoundError;
use Icinga\Repository\DbRepository;
use Icinga\Repository\RepositoryQuery;
use Icinga\User;
class DbUserGroupBackend extends DbRepository implements UserGroupBackendInterface
{
/**
* The query columns being provided
*
* @var array
*/
protected $queryColumns = array(
'group' => array(
'group_id' => 'g.id',
'group' => 'g.name COLLATE utf8_general_ci',
'group_name' => 'g.name',
'parent' => 'g.parent',
'created_at' => 'UNIX_TIMESTAMP(g.ctime)',
'last_modified' => 'UNIX_TIMESTAMP(g.mtime)'
),
'group_membership' => array(
'group_id' => 'gm.group_id',
'user' => 'gm.username COLLATE utf8_general_ci',
'user_name' => 'gm.username',
'created_at' => 'UNIX_TIMESTAMP(gm.ctime)',
'last_modified' => 'UNIX_TIMESTAMP(gm.mtime)'
)
);
/**
* The table aliases being applied
*
* @var array
*/
protected $tableAliases = array(
'group' => 'g',
'group_membership' => 'gm'
);
/**
* The statement columns being provided
*
* @var array
*/
protected $statementColumns = array(
'group' => array(
'group_id' => 'id',
'group_name' => 'name',
'parent' => 'parent',
'created_at' => 'ctime',
'last_modified' => 'mtime'
),
'group_membership' => array(
'group_id' => 'group_id',
'group_name' => 'group_id',
'user_name' => 'username',
'created_at' => 'ctime',
'last_modified' => 'mtime'
)
);
/**
* The columns which are not permitted to be queried
*
* @var array
*/
protected $filterColumns = array('group', 'user');
/**
* The value conversion rules to apply on a query or statement
*
* @var array
*/
protected $conversionRules = array(
'group' => array(
'parent' => 'group_id'
),
'group_membership' => array(
'group_name' => 'group_id'
)
);
/**
* Initialize this database user group backend
*/
protected function init()
{
if (! $this->ds->getTablePrefix()) {
$this->ds->setTablePrefix('icingaweb_');
}
}
/**
* Insert a table row with the given data
*
* @param string $table
* @param array $bind
*/
public function insert($table, array $bind)
{
$bind['created_at'] = date('Y-m-d H:i:s');
parent::insert($table, $bind);
}
/**
* Update table rows with the given data, optionally limited by using a filter
*
* @param string $table
* @param array $bind
* @param Filter $filter
*/
public function update($table, array $bind, Filter $filter = null)
{
$bind['last_modified'] = date('Y-m-d H:i:s');
parent::update($table, $bind, $filter);
}
/**
* Delete table rows, optionally limited by using a filter
*
* @param string $table
* @param Filter $filter
*/
public function delete($table, Filter $filter = null)
{
if ($table === 'group') {
parent::delete('group_membership', $filter);
$idQuery = $this->select(array('group_id'));
if ($filter !== null) {
$idQuery->applyFilter($filter);
}
$this->update('group', array('parent' => null), Filter::where('parent', $idQuery->fetchColumn()));
}
parent::delete($table, $filter);
}
/**
* Return the groups the given user is a member of
*
* @param User $user
*
* @return array
*/
public function getMemberships(User $user)
{
$groupQuery = $this->ds
->select()
->from(
array('g' => $this->prependTablePrefix('group')),
array(
'group_name' => 'g.name',
'parent_name' => 'gg.name'
)
)->joinLeft(
array('gg' => $this->prependTablePrefix('group')),
'g.parent = gg.id',
array()
);
$groups = array();
foreach ($groupQuery as $group) {
$groups[$group->group_name] = $group->parent_name;
}
$membershipQuery = $this
->select()
->from('group_membership', array('group_name'))
->where('user_name', $user->getUsername());
$memberships = array();
foreach ($membershipQuery as $membership) {
$memberships[] = $membership->group_name;
$parent = $groups[$membership->group_name];
while ($parent !== null) {
$memberships[] = $parent;
// Usually a parent is an existing group, but since we do not have a constraint on our table..
$parent = isset($groups[$parent]) ? $groups[$parent] : null;
}
}
return $memberships;
}
/**
* Join group into group_membership
*
* @param RepositoryQuery $query
*/
protected function joinGroup(RepositoryQuery $query)
{
$query->getQuery()->join(
$this->requireTable('group'),
'gm.group_id = g.id',
array()
);
}
/**
* Join group_membership into group
*
* @param RepositoryQuery $query
*/
protected function joinGroupMembership(RepositoryQuery $query)
{
$query->getQuery()->join(
$this->requireTable('group_membership'),
'g.id = gm.group_id',
array()
);
}
/**
* Fetch and return the corresponding id for the given group's name
*
* @param string|array $groupName
*
* @return int
*
* @throws NotFoundError
*/
protected function persistGroupId($groupName)
{
if (! $groupName || empty($groupName) || is_int($groupName)) {
return $groupName;
}
if (is_array($groupName)) {
if (is_int($groupName[0])) {
return $groupName; // In case the array contains mixed types...
}
$groupIds = $this->ds
->select()
->from($this->prependTablePrefix('group'), array('id'))
->where('name', $groupName)
->fetchColumn();
if (empty($groupIds)) {
throw new NotFoundError('No groups found matching one of: %s', implode(', ', $groupName));
}
return $groupIds;
}
$groupId = $this->ds
->select()
->from($this->prependTablePrefix('group'), array('id'))
->where('name', $groupName)
->fetchOne();
if ($groupId === false) {
throw new NotFoundError('Group "%s" does not exist', $groupName);
}
return $groupId;
}
}

View File

@ -0,0 +1,121 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Exception\StatementException;
use Icinga\Data\Filter\Filter;
use Icinga\Repository\IniRepository;
use Icinga\User;
use Icinga\Util\String;
class IniUserGroupBackend extends IniRepository implements UserGroupBackendInterface
{
/**
* The query columns being provided
*
* @var array
*/
protected $queryColumns = array(
'groups' => array(
'group' => 'name',
'group_name' => 'name',
'parent' => 'parent',
'created_at' => 'ctime',
'last_modified' => 'mtime',
'users'
)
);
/**
* The columns which are not permitted to be queried
*
* @var array
*/
protected $filterColumns = array('group');
/**
* The value conversion rules to apply on a query or statement
*
* @var array
*/
protected $conversionRules = array(
'groups' => array(
'created_at' => 'date_time',
'last_modified' => 'date_time',
'users' => 'comma_separated_string'
)
);
/**
* Initialize this ini user group backend
*/
protected function init()
{
$this->ds->getConfigObject()->setKeyColumn('name');
}
/**
* Add a new group to this backend
*
* @param string $target
* @param array $data
*
* @throws StatementException In case the operation has failed
*/
public function insert($target, array $data)
{
$data['created_at'] = time();
parent::insert($target, $data);
}
/**
* Update groups of this backend, optionally limited using a filter
*
* @param string $target
* @param array $data
* @param Filter $filter
*
* @throws StatementException In case the operation has failed
*/
public function update($target, array $data, Filter $filter = null)
{
$data['last_modified'] = time();
parent::update($target, $data, $filter);
}
/**
* Return the groups the given user is a member of
*
* @param User $user
*
* @return array
*/
public function getMemberships(User $user)
{
$result = $this->select()->fetchAll();
$groups = array();
foreach ($result as $group) {
$groups[$group->group_name] = $group->parent;
}
$username = strtolower($user->getUsername());
$memberships = array();
foreach ($result as $group) {
if ($group->users && !in_array($group->group_name, $memberships)) {
$users = array_map('strtolower', String::trimSplit($group->users));
if (in_array($username, $users)) {
$memberships[] = $group->group_name;
$parent = $groups[$group->group_name];
while ($parent !== null) {
$memberships[] = $parent;
$parent = isset($groups[$parent]) ? $groups[$parent] : null;
}
}
}
}
return $memberships;
}
}

View File

@ -0,0 +1,164 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication\UserGroup;
use Icinga\Application\Logger;
use Icinga\Application\Icinga;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
/**
* Factory for user group backends
*/
class UserGroupBackend
{
/**
* The default user group backend types provided by Icinga Web 2
*
* @var array
*/
protected static $defaultBackends = array(
'db',
//'ini'
);
/**
* The registered custom user group backends with their identifier as key and class name as value
*
* @var array
*/
protected static $customBackends;
/**
* Register all custom user group backends from all loaded modules
*/
public static function registerCustomUserGroupBackends()
{
if (static::$customBackends !== null) {
return;
}
static::$customBackends = array();
$providedBy = array();
foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) {
foreach ($module->getUserGroupBackends() as $identifier => $className) {
if (array_key_exists($identifier, $providedBy)) {
Logger::warning(
'Cannot register user group backend of type "%s" provided by module "%s".'
. ' The type is already provided by module "%s"',
$identifier,
$module->getName(),
$providedBy[$identifier]
);
} elseif (in_array($identifier, static::$defaultBackends)) {
Logger::warning(
'Cannot register user group backend of type "%s" provided by module "%s".'
. ' The type is a default type provided by Icinga Web 2',
$identifier,
$module->getName()
);
} else {
$providedBy[$identifier] = $module->getName();
static::$customBackends[$identifier] = $className;
}
}
}
}
/**
* Return the class for the given custom user group backend
*
* @param string $identifier The identifier of the custom user group backend
*
* @return string|null The name of the class or null in case there was no
* backend found with the given identifier
*
* @throws ConfigurationError In case the class associated to the given identifier does not exist
*/
protected static function getCustomUserGroupBackend($identifier)
{
static::registerCustomUserGroupBackends();
if (array_key_exists($identifier, static::$customBackends)) {
$className = static::$customBackends[$identifier];
if (! class_exists($className)) {
throw new ConfigurationError(
'Cannot utilize user group backend of type "%s". Class "%s" does not exist',
$identifier,
$className
);
}
return $className;
}
}
/**
* Create and return a user group backend with the given name and given configuration applied to it
*
* @param string $name
* @param ConfigObject $backendConfig
*
* @return UserGroupBackendInterface
*
* @throws ConfigurationError
*/
public static function create($name, ConfigObject $backendConfig)
{
if ($backendConfig->name !== null) {
$name = $backendConfig->name;
}
if (! ($backendType = strtolower($backendConfig->backend))) {
throw new ConfigurationError(
'Configuration for user group backend "%s" is missing the \'backend\' directive',
$name
);
}
if (in_array($backendType, static::$defaultBackends)) {
// The default backend check is the first one because of performance reasons:
// Do not attempt to load a custom user group backend unless it's actually required
} elseif (($customClass = static::getCustomUserGroupBackend($backendType)) !== null) {
$backend = new $customClass($backendConfig);
if (! is_a($backend, 'Icinga\Authentication\UserGroup\UserGroupBackendInterface')) {
throw new ConfigurationError(
'Cannot utilize user group backend of type "%s".'
. ' Class "%s" does not implement UserGroupBackendInterface',
$backendType,
$customClass
);
}
$backend->setName($name);
return $backend;
} else {
throw new ConfigurationError(
'Configuration for user group backend "%s" defines an invalid backend type.'
. ' Backend type "%s" is not supported',
$name,
$backendType
);
}
if ($backendConfig->resource === null) {
throw new ConfigurationError(
'Configuration for user group backend "%s" is missing the \'resource\' directive',
$name
);
}
$resource = ResourceFactory::create($backendConfig->resource);
switch ($backendType) {
case 'db':
$backend = new DbUserGroupBackend($resource);
break;
case 'ini':
$backend = new IniUserGroupBackend($resource);
break;
}
$backend->setName($name);
return $backend;
}
}

View File

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

View File

@ -1,112 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication;
use Icinga\Authentication\Backend\DbUserGroupBackend;
use Icinga\Authentication\Backend\IniUserGroupBackend;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\IcingaException;
use Icinga\User;
/**
* Base class and factory for user group backends
*/
abstract class UserGroupBackend
{
/**
* Name of the backend
*
* @var string
*/
protected $name;
/**
* Set the backend name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = (string) $name;
return $this;
}
/**
* Get the backend name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Create a user group backend
*
* @param string $name
* @param ConfigObject $backendConfig
*
* @return DbUserGroupBackend|IniUserGroupBackend
* @throws ConfigurationError If the backend configuration is invalid
*/
public static function create($name, ConfigObject $backendConfig)
{
if ($backendConfig->name !== null) {
$name = $backendConfig->name;
}
if (($backendType = $backendConfig->backend) === null) {
throw new ConfigurationError(
'Configuration for user group backend \'%s\' is missing the \'backend\' directive',
$name
);
}
$backendType = strtolower($backendType);
if (($resourceName = $backendConfig->resource) === null) {
throw new ConfigurationError(
'Configuration for user group backend \'%s\' is missing the \'resource\' directive',
$name
);
}
$resourceName = strtolower($resourceName);
try {
$resource = ResourceFactory::create($resourceName);
} catch (IcingaException $e) {
throw new ConfigurationError(
'Can\'t create user group backend \'%s\'. An exception was thrown: %s',
$resourceName,
$e
);
}
switch ($backendType) {
case 'db':
$backend = new DbUserGroupBackend($resource);
break;
case 'ini':
$backend = new IniUserGroupBackend($resource);
break;
default:
throw new ConfigurationError(
'Can\'t create user group backend \'%s\'. Invalid backend type \'%s\'.',
$name,
$backendType
);
}
$backend->setName($name);
return $backend;
}
/**
* Get the groups the given user is a member of
*
* @param User $user
*
* @return array
*/
abstract public function getMemberships(User $user);
}

View File

@ -4,22 +4,15 @@
namespace Icinga\Data;
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;

View File

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

View File

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

View File

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

View File

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

View File

@ -23,13 +23,11 @@ interface Fetchable
public function fetchRow();
/**
* 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

View File

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

View File

@ -4,7 +4,6 @@
namespace Icinga\Data;
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'
);
}
}

View File

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

View File

@ -0,0 +1,24 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\StatementException;
/**
* Interface for data updating
*/
interface Updatable
{
/**
* Update the target with the given data and optionally limit the affected entries by using a filter
*
* @param string $target
* @param array $data
* @param Filter $filter
*
* @throws StatementException
*/
public function update($target, array $data, Filter $filter = null);
}

View File

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

View File

@ -4,6 +4,7 @@
namespace Icinga\Protocol\File;
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.
*

View File

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

View File

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

View File

@ -0,0 +1,755 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Icinga\Data\Db\DbConnection;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Reducible;
use Icinga\Data\Updatable;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\StatementException;
use Icinga\Util\String;
/**
* Abstract base class for concrete database repository implementations
*
* Additionally provided features:
* <ul>
* <li>Support for table aliases</li>
* <li>Automatic table prefix handling</li>
* <li>Insert, update and delete capabilities</li>
* <li>Differentiation between statement and query columns</li>
* <li>Capability to join additional tables depending on the columns being selected or used in a filter</li>
* </ul>
*/
abstract class DbRepository extends Repository implements Extensible, Updatable, Reducible
{
/**
* The datasource being used
*
* @var DbConnection
*/
protected $ds;
/**
* The table aliases being applied
*
* This must be initialized by repositories which are going to make use of table aliases. Every table for which
* aliased columns are provided must be defined in this array using its name as key and the alias being used as
* value. Failure to do so will result in invalid queries.
*
* @var array
*/
protected $tableAliases;
/**
* The statement columns being provided
*
* This may be initialized by repositories which are going to make use of table aliases. It allows to provide
* alias-less column names to be used for a statement. The array needs to be in the following format:
* <pre><code>
* array(
* 'table_name' => array(
* 'column1',
* 'alias1' => 'column2',
* 'alias2' => 'column3'
* )
* )
* <pre><code>
*
* @var array
*/
protected $statementColumns;
/**
* An array to map table names to statement columns/aliases
*
* @var array
*/
protected $statementTableMap;
/**
* A flattened array to map statement columns to aliases
*
* @var array
*/
protected $statementColumnMap;
/**
* List of columns where the COLLATE SQL-instruction has been removed
*
* This list is being populated in case of a PostgreSQL backend only,
* to ensure case-insensitive string comparison in WHERE clauses.
*
* @var array
*/
protected $columnsWithoutCollation;
/**
* Create a new DB repository object
*
* In case $this->queryColumns has already been initialized, this initializes
* $this->columnsWithoutCollation in case of a PostgreSQL connection.
*
* @param DbConnection $ds The datasource to use
*/
public function __construct(DbConnection $ds)
{
parent::__construct($ds);
$this->columnsWithoutCollation = array();
if ($ds->getDbType() === 'pgsql' && $this->queryColumns !== null) {
$this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
}
}
/**
* Return the query columns being provided
*
* Initializes $this->columnsWithoutCollation in case of a PostgreSQL connection.
*
* @return array
*/
public function getQueryColumns()
{
if ($this->queryColumns === null) {
$this->queryColumns = parent::getQueryColumns();
if ($this->ds->getDbType() === 'pgsql') {
$this->queryColumns = $this->removeCollateInstruction($this->queryColumns);
}
}
return $this->queryColumns;
}
/**
* Return the table aliases to be applied
*
* Calls $this->initializeTableAliases() in case $this->tableAliases is null.
*
* @return array
*/
public function getTableAliases()
{
if ($this->tableAliases === null) {
$this->tableAliases = $this->initializeTableAliases();
}
return $this->tableAliases;
}
/**
* Overwrite this in your repository implementation in case you need to initialize the table aliases lazily
*
* @return array
*/
protected function initializeTableAliases()
{
return array();
}
/**
* Remove each COLLATE SQL-instruction from all given query columns
*
* @param array $queryColumns
*
* @return array $queryColumns, the updated version
*/
protected function removeCollateInstruction($queryColumns)
{
foreach ($queryColumns as & $columns) {
foreach ($columns as & $column) {
$column = preg_replace('/ COLLATE .+$/', '', $column, -1, $count);
if ($count > 0) {
$this->columnsWithoutCollation[] = $column;
}
}
}
return $queryColumns;
}
/**
* Return the given table with the datasource's prefix being prepended
*
* @param array|string $table
*
* @return array|string
*
* @throws IcingaException In case $table is not of a supported type
*/
protected function prependTablePrefix($table)
{
$prefix = $this->ds->getTablePrefix();
if (! $prefix) {
return $table;
}
if (is_array($table)) {
foreach ($table as & $tableName) {
if (strpos($tableName, $prefix) === false) {
$tableName = $prefix . $tableName;
}
}
} elseif (is_string($table)) {
$table = (strpos($table, $prefix) === false ? $prefix : '') . $table;
} else {
throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
}
return $table;
}
/**
* Remove the datasource's prefix from the given table name and return the remaining part
*
* @param array|string $table
*
* @return array|string
*
* @throws IcingaException In case $table is not of a supported type
*/
protected function removeTablePrefix($table)
{
$prefix = $this->ds->getTablePrefix();
if (! $prefix) {
return $table;
}
if (is_array($table)) {
foreach ($table as & $tableName) {
if (strpos($tableName, $prefix) === 0) {
$tableName = str_replace($prefix, '', $tableName);
}
}
} elseif (is_string($table)) {
if (strpos($table, $prefix) === 0) {
$table = str_replace($prefix, '', $table);
}
} else {
throw new IcingaException('Table prefix handling for type "%s" is not supported', type($table));
}
return $table;
}
/**
* Return the given table with its alias being applied
*
* @param array|string $table
*
* @return array|string
*/
protected function applyTableAlias($table)
{
$tableAliases = $this->getTableAliases();
if (is_array($table) || !isset($tableAliases[($nonPrefixedTable = $this->removeTablePrefix($table))])) {
return $table;
}
return array($tableAliases[$nonPrefixedTable] => $table);
}
/**
* Return the given table with its alias being cleared
*
* @param array|string $table
*
* @return string
*
* @throws IcingaException In case $table is not of a supported type
*/
protected function clearTableAlias($table)
{
if (is_string($table)) {
return $table;
}
if (is_array($table)) {
return reset($table);
}
throw new IcingaException('Table alias handling for type "%s" is not supported', type($table));
}
/**
* Insert a table row with the given data
*
* @param string $table
* @param array $bind
*/
public function insert($table, array $bind)
{
$this->ds->insert($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind));
}
/**
* Update table rows with the given data, optionally limited by using a filter
*
* @param string $table
* @param array $bind
* @param Filter $filter
*/
public function update($table, array $bind, Filter $filter = null)
{
if ($filter) {
$filter = $this->requireFilter($table, $filter);
}
$this->ds->update($this->prependTablePrefix($table), $this->requireStatementColumns($table, $bind), $filter);
}
/**
* Delete table rows, optionally limited by using a filter
*
* @param string $table
* @param Filter $filter
*/
public function delete($table, Filter $filter = null)
{
if ($filter) {
$filter = $this->requireFilter($table, $filter);
}
$this->ds->delete($this->prependTablePrefix($table), $filter);
}
/**
* Return the statement columns being provided
*
* Calls $this->initializeStatementColumns() in case $this->statementColumns is null.
*
* @return array
*/
public function getStatementColumns()
{
if ($this->statementColumns === null) {
$this->statementColumns = $this->initializeStatementColumns();
}
return $this->statementColumns;
}
/**
* Overwrite this in your repository implementation in case you need to initialize the statement columns lazily
*
* @return array
*/
protected function initializeStatementColumns()
{
return array();
}
/**
* Return an array to map table names to statement columns/aliases
*
* @return array
*/
protected function getStatementTableMap()
{
if ($this->statementTableMap === null) {
$this->initializeStatementMaps();
}
return $this->statementTableMap;
}
/**
* Return a flattened array to map statement columns to aliases
*
* @return array
*/
protected function getStatementColumnMap()
{
if ($this->statementColumnMap === null) {
$this->initializeStatementMaps();
}
return $this->statementColumnMap;
}
/**
* Initialize $this->statementTableMap and $this->statementColumnMap
*/
protected function initializeStatementMaps()
{
$this->statementTableMap = array();
$this->statementColumnMap = array();
foreach ($this->getStatementColumns() as $table => $columns) {
foreach ($columns as $alias => $column) {
$key = is_string($alias) ? $alias : $column;
if (array_key_exists($key, $this->statementTableMap)) {
if ($this->statementTableMap[$key] !== null) {
$existingTable = $this->statementTableMap[$key];
$existingColumn = $this->statementColumnMap[$key];
$this->statementTableMap[$existingTable . '.' . $key] = $existingTable;
$this->statementColumnMap[$existingTable . '.' . $key] = $existingColumn;
$this->statementTableMap[$key] = null;
$this->statementColumnMap[$key] = null;
}
$this->statementTableMap[$table . '.' . $key] = $table;
$this->statementColumnMap[$table . '.' . $key] = $column;
} else {
$this->statementTableMap[$key] = $table;
$this->statementColumnMap[$key] = $column;
}
}
}
}
/**
* Return whether this repository is capable of converting values
*
* This does not check whether any conversion for the given table is available, as it may be possible
* that columns from another table where joined in which would otherwise not being converted.
*
* @param array|string $table
*
* @return bool
*/
public function providesValueConversion($_)
{
$conversionRules = $this->getConversionRules();
return !empty($conversionRules);
}
/**
* Return the name of the conversion method for the given alias or column name and context
*
* @param array|string $table The datasource's table
* @param string $name The alias or column name for which to return a conversion method
* @param string $context The context of the conversion: persist or retrieve
*
* @return string
*
* @throws ProgrammingError In case a conversion rule is found but not any conversion method
*/
protected function getConverter($table, $name, $context)
{
if (
$this->validateQueryColumnAssociation($table, $name)
|| $this->validateStatementColumnAssociation($table, $name)
) {
$table = $this->removeTablePrefix($this->clearTableAlias($table));
} else {
$table = $this->findTableName($name);
if (! $table) {
throw new ProgrammingError('Column name validation seems to have failed. Did you require the column?');
}
}
return parent::getConverter($table, $name, $context);
}
/**
* Validate that the requested table exists
*
* This will prepend the datasource's table prefix and will apply the table's alias, if any.
*
* @param string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context
* (unused by the base implementation)
*
* @return array|string
*
* @throws ProgrammingError In case the given table does not exist
*/
public function requireTable($table, RepositoryQuery $query = null)
{
$statementColumns = $this->getStatementColumns();
if (! isset($statementColumns[$table])) {
$table = parent::requireTable($table);
}
return $this->prependTablePrefix($this->applyTableAlias($table));
}
/**
* Recurse the given filter, require each column for the given table and convert all values
*
* In case of a PostgreSQL connection, this applies LOWER() on the column and strtolower()
* on the value if a COLLATE SQL-instruction is part of the resolved column.
*
* @param string $table The table being filtered
* @param Filter $filter The filter to recurse
* @param RepositoryQuery $query An optional query to pass as context
* (Directly passed through to $this->requireFilterColumn)
* @param bool $clone Whether to clone $filter first
*
* @return Filter The udpated filter
*/
public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
{
$filter = parent::requireFilter($table, $filter, $query, $clone);
if ($filter->isExpression()) {
$column = $filter->getColumn();
if (in_array($column, $this->columnsWithoutCollation) && strpos($column, 'LOWER') !== 0) {
$filter->setColumn('LOWER(' . $column . ')');
$expression = $filter->getExpression();
if (is_array($expression)) {
$filter->setExpression(array_map('strtolower', $expression));
} else {
$filter->setExpression(strtolower($expression));
}
}
}
return $filter;
}
/**
* Return this repository's query columns of the given table mapped to their respective aliases
*
* @param array|string $table
*
* @return array
*
* @throws ProgrammingError In case $table does not exist
*/
public function requireAllQueryColumns($table)
{
return parent::requireAllQueryColumns($this->removeTablePrefix($this->clearTableAlias($table)));
}
/**
* Return the query column name for the given alias or null in case the alias does not exist
*
* @param array|string $table
* @param string $alias
*
* @return string|null
*/
public function resolveQueryColumnAlias($table, $alias)
{
return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias);
}
/**
* Return whether the given query column name or alias is available in the given table
*
* @param array|string $table
* @param string $column
*
* @return bool
*/
public function validateQueryColumnAssociation($table, $column)
{
return parent::validateQueryColumnAssociation(
$this->removeTablePrefix($this->clearTableAlias($table)),
$column
);
}
/**
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
*
* Attempts to join the given column from a different table if its association to the given table cannot be
* verified.
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context,
* if not given no join will be attempted
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid query column
*/
public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
{
if ($query === null || $this->validateQueryColumnAssociation($table, $name)) {
return parent::requireQueryColumn($table, $name, $query);
}
return $this->joinColumn($name, $table, $query);
}
/**
* Validate that the given column is a valid filter target and return it or the actual name if it's an alias
*
* Attempts to join the given column from a different table if its association to the given table cannot be
* verified.
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context,
* if not given the column is considered being used for a statement filter
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid filter column
*/
public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
{
if ($query === null) {
return $this->requireStatementColumn($table, $name);
}
if ($this->validateQueryColumnAssociation($table, $name)) {
return parent::requireFilterColumn($table, $name, $query);
}
return $this->joinColumn($name, $table, $query);
}
/**
* Return the statement column name for the given alias or null in case the alias does not exist
*
* @param string $table
* @param string $alias
*
* @return string|null
*/
public function resolveStatementColumnAlias($table, $alias)
{
$statementColumnMap = $this->getStatementColumnMap();
if (isset($statementColumnMap[$alias])) {
return $statementColumnMap[$alias];
}
$prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
if (isset($statementColumnMap[$prefixedAlias])) {
return $statementColumnMap[$prefixedAlias];
}
}
/**
* Return whether the given alias or statement column name is available in the given table
*
* @param string $table
* @param string $alias
*
* @return bool
*/
public function validateStatementColumnAssociation($table, $alias)
{
$statementTableMap = $this->getStatementTableMap();
if (isset($statementTableMap[$alias])) {
return $statementTableMap[$alias] === $this->removeTablePrefix($table);
}
$prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
return isset($statementTableMap[$prefixedAlias]);
}
/**
* Return whether the given column name or alias of the given table is a valid statement column
*
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasStatementColumn($table, $name)
{
if (
$this->resolveStatementColumnAlias($table, $name) === null
|| !$this->validateStatementColumnAssociation($table, $name)
) {
return parent::hasStatementColumn($table, $name);
}
return true;
}
/**
* Validate that the given column is a valid statement column and return it or the actual name if it's an alias
*
* @param string $table The table for which to require the column
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
* @throws StatementException In case the given column is not a statement column
*/
public function requireStatementColumn($table, $name)
{
if (($column = $this->resolveStatementColumnAlias($table, $name)) === null) {
return parent::requireStatementColumn($table, $name);
}
if (! $this->validateStatementColumnAssociation($table, $name)) {
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
}
return $column;
}
/**
* Join alias or column $name into $table using $query
*
* Attempts to find a valid table for the given alias or column name and a method labelled join<TableName>
* to process the actual join logic. If neither of those is found, ProgrammingError will be thrown.
* The method is called with the same parameters but in reversed order.
*
* @param string $name The alias or column name to join into $target
* @param array|string $target The table to join $name into
* @param RepositoryQUery $query The query to apply the JOIN-clause on
*
* @return string The resolved alias or $name
*
* @throws ProgrammingError In case no valid table or join<TableName>-method is found
*/
public function joinColumn($name, $target, RepositoryQuery $query)
{
$tableName = $this->findTableName($name);
if (! $tableName) {
throw new ProgrammingError(
'Unable to find a valid table for column "%s" to join into "%s"',
$name,
$this->removeTablePrefix($this->clearTableAlias($target))
);
}
$column = $this->resolveQueryColumnAlias($tableName, $name);
$prefixedTableName = $this->prependTablePrefix($tableName);
if ($query->getQuery()->hasJoinedTable($prefixedTableName)) {
return $column;
}
$joinMethod = 'join' . String::cname($tableName);
if (! method_exists($this, $joinMethod)) {
throw new ProgrammingError(
'Unable to join table "%s" into "%s". Method "%s" not found',
$tableName,
$this->removeTablePrefix($this->clearTableAlias($target)),
$joinMethod
);
}
$this->$joinMethod($query, $target, $name);
return $column;
}
/**
* Return the table name for the given alias or column name
*
* @param string $column
*
* @return string|null null in case no table is found
*/
protected function findTableName($column)
{
$aliasTableMap = $this->getAliasTableMap();
if (isset($aliasTableMap[$column])) {
return $aliasTableMap[$column];
}
// TODO(jom): Elaborate whether it makes sense to throw ProgrammingError
// instead (duplicate aliases in different tables?)
foreach ($aliasTableMap as $alias => $table) {
if (strpos($alias, '.') !== false) {
list($_, $alias) = explode('.', $column, 2);
if ($alias === $column) {
return $table;
}
}
}
}
}

View File

@ -0,0 +1,186 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Exception;
use Icinga\Application\Config;
use Icinga\Data\Extensible;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Updatable;
use Icinga\Data\Reducible;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\StatementException;
/**
* Abstract base class for concrete INI repository implementations
*
* Additionally provided features:
* <ul>
* <li>Insert, update and delete capabilities</li>
* </ul>
*/
abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
{
/**
* The datasource being used
*
* @var Config
*/
protected $ds;
/**
* Create a new INI repository object
*
* @param Config $ds The data source to use
*
* @throws ProgrammingError In case the given data source does not provide a valid key column
*/
public function __construct(Config $ds)
{
parent::__construct($ds); // First! Due to init().
if (! $ds->getConfigObject()->getKeyColumn()) {
throw new ProgrammingError('INI repositories require their data source to provide a valid key column');
}
}
/**
* Insert the given data for the given target
*
* $data must provide a proper value for the data source's key column.
*
* @param string $target
* @param array $data
*
* @throws StatementException In case the operation has failed
*/
public function insert($target, array $data)
{
$newData = $this->requireStatementColumns($target, $data);
$section = $this->extractSectionName($newData);
if ($this->ds->hasSection($section)) {
throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
}
$this->ds->setSection($section, $newData);
try {
$this->ds->saveIni();
} catch (Exception $e) {
throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage());
}
}
/**
* Update the target with the given data and optionally limit the affected entries by using a filter
*
* @param string $target
* @param array $data
* @param Filter $filter
*
* @throws StatementException In case the operation has failed
*/
public function update($target, array $data, Filter $filter = null)
{
$newData = $this->requireStatementColumns($target, $data);
$keyColumn = $this->ds->getConfigObject()->getKeyColumn();
if ($filter === null && isset($newData[$keyColumn])) {
throw new StatementException(
t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
$keyColumn
);
}
if ($filter !== null) {
$filter = $this->requireFilter($target, $filter);
}
$newSection = null;
foreach (iterator_to_array($this->ds) as $section => $config) {
if ($filter !== null && !$filter->matches($config)) {
continue;
}
if ($newSection !== null) {
throw new StatementException(
t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
$keyColumn
);
}
foreach ($newData as $column => $value) {
if ($column === $keyColumn) {
$newSection = $value;
} else {
$config->$column = $value;
}
}
if ($newSection) {
if ($this->ds->hasSection($newSection)) {
throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection);
}
$this->ds->removeSection($section)->setSection($newSection, $config);
} else {
$this->ds->setSection($section, $config);
}
}
try {
$this->ds->saveIni();
} catch (Exception $e) {
throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage());
}
}
/**
* Delete entries in the given target, optionally limiting the affected entries by using a filter
*
* @param string $target
* @param Filter $filter
*
* @throws StatementException In case the operation has failed
*/
public function delete($target, Filter $filter = null)
{
if ($filter !== null) {
$filter = $this->requireFilter($target, $filter);
}
foreach (iterator_to_array($this->ds) as $section => $config) {
if ($filter === null || $filter->matches($config)) {
$this->ds->removeSection($section);
}
}
try {
$this->ds->saveIni();
} catch (Exception $e) {
throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage());
}
}
/**
* Extract and return the section name off of the given $data
*
* @param array $data
*
* @return string
*
* @throws ProgrammingError In case no valid section name is available
*/
protected function extractSectionName(array & $data)
{
$keyColumn = $this->ds->getConfigObject()->getKeyColumn();
if (! isset($data[$keyColumn])) {
throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn);
}
$section = $data[$keyColumn];
unset($data[$keyColumn]);
return $section;
}
}

View File

@ -0,0 +1,861 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use DateTime;
use Icinga\Application\Logger;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Selectable;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\QueryException;
use Icinga\Exception\StatementException;
use Icinga\Util\String;
/**
* Abstract base class for concrete repository implementations
*
* To utilize this class and its features, the following is required:
* <ul>
* <li>Concrete implementations need to initialize Repository::$queryColumns</li>
* <li>The datasource passed to a repository must implement the Selectable interface</li>
* <li>The datasource must yield an instance of Queryable when its select() method is called</li>
* </ul>
*/
abstract class Repository implements Selectable
{
/**
* The format to use when converting values of type date_time
*/
const DATETIME_FORMAT = 'd/m/Y g:i A';
/**
* The name of this repository
*
* @var string
*/
protected $name;
/**
* The datasource being used
*
* @var Selectable
*/
protected $ds;
/**
* The base table name this repository is responsible for
*
* This will be automatically set to the first key of $queryColumns if not explicitly set.
*
* @var string
*/
protected $baseTable;
/**
* The query columns being provided
*
* This must be initialized by concrete repository implementations, in the following format
* <pre><code>
* array(
* 'baseTable' => array(
* 'column1',
* 'alias1' => 'column2',
* 'alias2' => 'column3'
* )
* )
* <pre><code>
*
* @var array
*/
protected $queryColumns;
/**
* The columns (or aliases) which are not permitted to be queried. (by design)
*
* @var array An array of strings
*/
protected $filterColumns;
/**
* The default sort rules to be applied on a query
*
* This may be initialized by concrete repository implementations, in the following format
* <pre><code>
* array(
* 'alias_or_column_name' => array(
* 'order' => 'asc'
* ),
* 'alias_or_column_name' => array(
* 'columns' => array(
* 'once_more_the_alias_or_column_name_as_in_the_parent_key',
* 'an_additional_alias_or_column_name_with_a_specific_direction asc'
* ),
* 'order' => 'desc'
* ),
* 'alias_or_column_name' => array(
* 'columns' => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column')
* // Ascendant sort by default
* )
* )
* <pre><code>
* Note that it's mandatory to supply the alias name in case there is one.
*
* @var array
*/
protected $sortRules;
/**
* The value conversion rules to apply on a query or statement
*
* This may be initialized by concrete repository implementations and describes for which aliases or column
* names what type of conversion is available. For entries, where the key is the alias/column and the value
* is the type identifier, the repository attempts to find a conversion method for the alias/column first and,
* if none is found, then for the type. If an entry only provides a value, which is the alias/column, the
* repository only attempts to find a conversion method for the alias/column. The name of a conversion method
* is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and
* groupname will be translated to retrieveGroupname)
*
* @var array
*/
protected $conversionRules;
/**
* An array to map table names to aliases
*
* @var array
*/
protected $aliasTableMap;
/**
* A flattened array to map query columns to aliases
*
* @var array
*/
protected $aliasColumnMap;
/**
* Create a new repository object
*
* @param Selectable $ds The datasource to use
*/
public function __construct(Selectable $ds)
{
$this->ds = $ds;
$this->aliasTableMap = array();
$this->aliasColumnMap = array();
$this->init();
}
/**
* Initialize this repository
*
* Supposed to be overwritten by concrete repository implementations.
*/
protected function init()
{
}
/**
* Set this repository's name
*
* @param string $name
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Return this repository's name
*
* In case no name has been explicitly set yet, the class name is returned.
*
* @return string
*/
public function getName()
{
return $this->name ?: __CLASS__;
}
/**
* Return the datasource being used
*
* @return Selectable
*/
public function getDataSource()
{
return $this->ds;
}
/**
* Return the base table name this repository is responsible for
*
* @return string
*
* @throws ProgrammingError In case no base table name has been set and
* $this->queryColumns does not provide one either
*/
public function getBaseTable()
{
if ($this->baseTable === null) {
$queryColumns = $this->getQueryColumns();
reset($queryColumns);
$this->baseTable = key($queryColumns);
if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) {
throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable);
}
}
return $this->baseTable;
}
/**
* Return the query columns being provided
*
* Calls $this->initializeQueryColumns() in case $this->queryColumns is null.
*
* @return array
*/
public function getQueryColumns()
{
if ($this->queryColumns === null) {
$this->queryColumns = $this->initializeQueryColumns();
}
return $this->queryColumns;
}
/**
* Overwrite this in your repository implementation in case you need to initialize the query columns lazily
*
* @return array
*/
protected function initializeQueryColumns()
{
return array();
}
/**
* Return the columns (or aliases) which are not permitted to be queried
*
* Calls $this->initializeFilterColumns() in case $this->filterColumns is null.
*
* @return array
*/
public function getFilterColumns()
{
if ($this->filterColumns === null) {
$this->filterColumns = $this->initializeFilterColumns();
}
return $this->filterColumns;
}
/**
* Overwrite this in your repository implementation in case you need to initialize the filter columns lazily
*
* @return array
*/
protected function initializeFilterColumns()
{
return array();
}
/**
* Return the default sort rules to be applied on a query
*
* Calls $this->initializeSortRules() in case $this->sortRules is null.
*
* @return array
*/
public function getSortRules()
{
if ($this->sortRules === null) {
$this->sortRules = $this->initializeSortRules();
}
return $this->sortRules;
}
/**
* Overwrite this in your repository implementation in case you need to initialize the sort rules lazily
*
* @return array
*/
protected function initializeSortRules()
{
return array();
}
/**
* Return the value conversion rules to apply on a query
*
* Calls $this->initializeConversionRules() in case $this->conversionRules is null.
*
* @return array
*/
public function getConversionRules()
{
if ($this->conversionRules === null) {
$this->conversionRules = $this->initializeConversionRules();
}
return $this->conversionRules;
}
/**
* Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily
*
* @return array
*/
protected function initializeConversionRules()
{
return array();
}
/**
* Return an array to map table names to aliases
*
* @return array
*/
protected function getAliasTableMap()
{
if (empty($this->aliasTableMap)) {
$this->initializeAliasMaps();
}
return $this->aliasTableMap;
}
/**
* Return a flattened array to map query columns to aliases
*
* @return array
*/
protected function getAliasColumnMap()
{
if (empty($this->aliasColumnMap)) {
$this->initializeAliasMaps();
}
return $this->aliasColumnMap;
}
/**
* Initialize $this->aliasTableMap and $this->aliasColumnMap
*
* @throws ProgrammingError In case $this->queryColumns does not provide any column information
*/
protected function initializeAliasMaps()
{
$queryColumns = $this->getQueryColumns();
if (empty($queryColumns)) {
throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
}
foreach ($queryColumns as $table => $columns) {
foreach ($columns as $alias => $column) {
if (! is_string($alias)) {
$key = $column;
} else {
$key = $alias;
$column = preg_replace('~\n\s*~', ' ', $column);
}
if (array_key_exists($key, $this->aliasTableMap)) {
if ($this->aliasTableMap[$key] !== null) {
$existingTable = $this->aliasTableMap[$key];
$existingColumn = $this->aliasColumnMap[$key];
$this->aliasTableMap[$existingTable . '.' . $key] = $existingTable;
$this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
$this->aliasTableMap[$key] = null;
$this->aliasColumnMap[$key] = null;
}
$this->aliasTableMap[$table . '.' . $key] = $table;
$this->aliasColumnMap[$table . '.' . $key] = $column;
} else {
$this->aliasTableMap[$key] = $table;
$this->aliasColumnMap[$key] = $column;
}
}
}
}
/**
* Return a new query for the given columns
*
* @param array $columns The desired columns, if null all columns will be queried
*
* @return RepositoryQuery
*/
public function select(array $columns = null)
{
$query = new RepositoryQuery($this);
$query->from($this->getBaseTable(), $columns);
return $query;
}
/**
* Return whether this repository is capable of converting values for the given table
*
* @param string $table
*
* @return bool
*/
public function providesValueConversion($table)
{
$conversionRules = $this->getConversionRules();
return !empty($conversionRules) && isset($conversionRules[$table]);
}
/**
* Convert a value supposed to be transmitted to the data source
*
* @param string $table The table where to persist the value
* @param string $name The alias or column name
* @param mixed $value The value to convert
*
* @return mixed If conversion was possible, the converted value, otherwise the unchanged value
*/
public function persistColumn($table, $name, $value)
{
$converter = $this->getConverter($table, $name, 'persist');
if ($converter !== null) {
$value = $this->$converter($value);
}
return $value;
}
/**
* Convert a value which was fetched from the data source
*
* @param string $table The table the value has been fetched from
* @param string $name The alias or column name
* @param mixed $value The value to convert
*
* @return mixed If conversion was possible, the converted value, otherwise the unchanged value
*/
public function retrieveColumn($table, $name, $value)
{
$converter = $this->getConverter($table, $name, 'retrieve');
if ($converter !== null) {
$value = $this->$converter($value);
}
return $value;
}
/**
* Return the name of the conversion method for the given alias or column name and context
*
* @param string $table The datasource's table
* @param string $name The alias or column name for which to return a conversion method
* @param string $context The context of the conversion: persist or retrieve
*
* @return string
*
* @throws ProgrammingError In case a conversion rule is found but not any conversion method
*/
protected function getConverter($table, $name, $context)
{
$conversionRules = $this->getConversionRules();
if (! isset($conversionRules[$table])) {
return;
}
$tableRules = $conversionRules[$table];
// Check for a conversion method for the alias/column first
if (array_key_exists($name, $tableRules) || in_array($name, $tableRules)) {
$methodName = $context . join('', array_map('ucfirst', explode('_', $name)));
if (method_exists($this, $methodName)) {
return $methodName;
}
}
// The conversion method for the type is just a fallback, but it is required to exist if defined
if (isset($tableRules[$name])) {
$identifier = join('', array_map('ucfirst', explode('_', $tableRules[$name])));
if (! method_exists($this, $context . $identifier)) {
// Do not throw an error in case at least one conversion method exists
if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) {
throw new ProgrammingError(
'Cannot find any conversion method for type "%s"'
. '. Add a proper conversion method or remove the type definition',
$tableRules[$name]
);
}
Logger::debug(
'Conversion method "%s" for type definition "%s" does not exist in repository "%s".',
$context . $identifier,
$tableRules[$name],
$this->getName()
);
} else {
return $context . $identifier;
}
}
}
/**
* Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT
*
* @param mixed $value
*
* @return string
*/
protected function persistDateTime($value)
{
if (is_numeric($value)) {
$value = date(static::DATETIME_FORMAT, $value);
} elseif ($value instanceof DateTime) {
$value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone
} elseif ($value !== null) {
throw new ProgrammingError(
'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object',
$value
);
}
return $value;
}
/**
* Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp
*
* @param string $value
*
* @return int
*/
protected function retrieveDateTime($value)
{
if (is_numeric($value)) {
$value = (int) $value;
} elseif (is_string($value)) {
$dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value);
if ($dateTime === false) {
Logger::debug(
'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"',
$value,
static::DATETIME_FORMAT,
$this->getName()
);
$value = null;
} else {
$value = $dateTime->getTimestamp();
}
} elseif ($value !== null) {
throw new ProgrammingError(
'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string',
$value
);
}
return $value;
}
/**
* Convert the given array to an comma separated string
*
* @param array|string $value
*
* @return string
*/
protected function persistCommaSeparatedString($value)
{
if (is_array($value)) {
$value = join(',', array_map('trim', $value));
} elseif ($value !== null && !is_string($value)) {
throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value);
}
return $value;
}
/**
* Convert the given comma separated string to an array
*
* @param string $value
*
* @return array
*/
protected function retrieveCommaSeparatedString($value)
{
if ($value && is_string($value)) {
$value = String::trimSplit($value);
} elseif ($value !== null) {
throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value);
}
return $value;
}
/**
* Validate that the requested table exists
*
* @param string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context
* (unused by the base implementation)
*
* @return string The table's name, may differ from the given one
*
* @throws ProgrammingError In case the given table does not exist
*/
public function requireTable($table, RepositoryQuery $query = null)
{
$queryColumns = $this->getQueryColumns();
if (! isset($queryColumns[$table])) {
throw new ProgrammingError('Table "%s" not found', $table);
}
return $table;
}
/**
* Recurse the given filter, require each column for the given table and convert all values
*
* @param string $table The table being filtered
* @param Filter $filter The filter to recurse
* @param RepositoryQuery $query An optional query to pass as context
* (Directly passed through to $this->requireFilterColumn)
* @param bool $clone Whether to clone $filter first
*
* @return Filter The udpated filter
*/
public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
{
if ($clone) {
$filter = clone $filter;
}
if ($filter->isExpression()) {
$column = $filter->getColumn();
$filter->setColumn($this->requireFilterColumn($table, $column, $query));
$filter->setExpression($this->persistColumn($table, $column, $filter->getExpression()));
} elseif ($filter->isChain()) {
foreach ($filter->filters() as $chainOrExpression) {
$this->requireFilter($table, $chainOrExpression, $query, false);
}
}
return $filter;
}
/**
* Return this repository's query columns of the given table mapped to their respective aliases
*
* @param string $table
*
* @return array
*
* @throws ProgrammingError In case $table does not exist
*/
public function requireAllQueryColumns($table)
{
$queryColumns = $this->getQueryColumns();
if (! array_key_exists($table, $queryColumns)) {
throw new ProgrammingError('Table name "%s" not found', $table);
}
$filterColumns = $this->getFilterColumns();
$columns = array();
foreach ($queryColumns[$table] as $alias => $column) {
if (! in_array(is_string($alias) ? $alias : $column, $filterColumns)) {
$columns[$alias] = $column;
}
}
return $columns;
}
/**
* Return the query column name for the given alias or null in case the alias does not exist
*
* @param string $table
* @param string $alias
*
* @return string|null
*/
public function resolveQueryColumnAlias($table, $alias)
{
$aliasColumnMap = $this->getAliasColumnMap();
if (isset($aliasColumnMap[$alias])) {
return $aliasColumnMap[$alias];
}
$prefixedAlias = $table . '.' . $alias;
if (isset($aliasColumnMap[$prefixedAlias])) {
return $aliasColumnMap[$prefixedAlias];
}
}
/**
* Return whether the given alias or query column name is available in the given table
*
* @param string $table
* @param string $alias
*
* @return bool
*/
public function validateQueryColumnAssociation($table, $alias)
{
$aliasTableMap = $this->getAliasTableMap();
if (isset($aliasTableMap[$alias])) {
return $aliasTableMap[$alias] === $table;
}
$prefixedAlias = $table . '.' . $alias;
return isset($aliasTableMap[$prefixedAlias]);
}
/**
* Return whether the given column name or alias is a valid query column
*
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasQueryColumn($table, $name)
{
if (in_array($name, $this->getFilterColumns())) {
return false;
}
return $this->resolveQueryColumnAlias($table, $name) !== null
&& $this->validateQueryColumnAssociation($table, $name);
}
/**
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid query column
*/
public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
{
if (in_array($name, $this->getFilterColumns())) {
throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
}
if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
throw new QueryException(t('Query column "%s" not found'), $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
}
return $column;
}
/**
* Return whether the given column name or alias is a valid filter column
*
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasFilterColumn($table, $name)
{
return $this->resolveQueryColumnAlias($table, $name) !== null
&& $this->validateQueryColumnAssociation($table, $name);
}
/**
* Validate that the given column is a valid filter target and return it or the actual name if it's an alias
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid filter column
*/
public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
{
if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
throw new QueryException(t('Filter column "%s" not found'), $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
}
return $column;
}
/**
* Return whether the given column name or alias of the given table is a valid statement column
*
* @param string $table The table where to look for the column or alias
* @param string $name The column name or alias to check
*
* @return bool
*/
public function hasStatementColumn($table, $name)
{
return $this->hasQueryColumn($table, $name);
}
/**
* Validate that the given column is a valid statement column and return it or the actual name if it's an alias
*
* @param string $table The table for which to require the column
* @param string $name The name or alias of the column to validate
*
* @return string The given column's name
*
* @throws StatementException In case the given column is not a statement column
*/
public function requireStatementColumn($table, $name)
{
if (in_array($name, $this->filterColumns)) {
throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
}
if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
throw new StatementException('Statement column "%s" not found', $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
}
return $column;
}
/**
* Resolve the given aliases or column names of the given table supposed to be persisted and convert their values
*
* @param string $table
* @param array $data
*
* @return array
*/
public function requireStatementColumns($table, array $data)
{
$resolved = array();
foreach ($data as $alias => $value) {
$resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value);
}
return $resolved;
}
}

View File

@ -0,0 +1,589 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Repository;
use Iterator;
use IteratorAggregate;
use Icinga\Application\Benchmark;
use Icinga\Application\Logger;
use Icinga\Data\QueryInterface;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\QueryException;
/**
* Query class supposed to mediate between a repository and its datasource's query
*/
class RepositoryQuery implements QueryInterface, Iterator
{
/**
* The repository being used
*
* @var Repository
*/
protected $repository;
/**
* The real query being used
*
* @var QueryInterface
*/
protected $query;
/**
* The current target to be queried
*
* @var mixed
*/
protected $target;
/**
* The real query's iterator
*
* @var Iterator
*/
protected $iterator;
/**
* Create a new repository query
*
* @param Repository $repository The repository to use
*/
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
/**
* Return the real query being used
*
* @return QueryInterface
*/
public function getQuery()
{
return $this->query;
}
/**
* Set where to fetch which columns
*
* This notifies the repository about each desired query column.
*
* @param mixed $target The target from which to fetch the columns
* @param array $columns If null or an empty array, all columns will be fetched
*
* @return $this
*/
public function from($target, array $columns = null)
{
$target = $this->repository->requireTable($target, $this);
$this->query = $this->repository->getDataSource()->select()->from($target);
$this->query->columns($this->prepareQueryColumns($target, $columns));
$this->target = $target;
return $this;
}
/**
* Return the columns to fetch
*
* @return array
*/
public function getColumns()
{
return $this->query->getColumns();
}
/**
* Set which columns to fetch
*
* This notifies the repository about each desired query column.
*
* @param array $columns If null or an empty array, all columns will be fetched
*
* @return $this
*/
public function columns(array $columns)
{
$this->query->columns($this->prepareQueryColumns($this->target, $columns));
return $this;
}
/**
* Resolve the given columns supposed to be fetched
*
* This notifies the repository about each desired query column.
*
* @param mixed $target The target where to look for each column
* @param array $desiredColumns Pass null or an empty array to require all query columns
*
* @return array The desired columns indexed by their respective alias
*/
protected function prepareQueryColumns($target, array $desiredColumns = null)
{
if (empty($desiredColumns)) {
$columns = $this->repository->requireAllQueryColumns($target);
} else {
$columns = array();
foreach ($desiredColumns as $customAlias => $columnAlias) {
$resolvedColumn = $this->repository->requireQueryColumn($target, $columnAlias, $this);
if ($resolvedColumn !== $columnAlias) {
$columns[is_string($customAlias) ? $customAlias : $columnAlias] = $resolvedColumn;
} elseif (is_string($customAlias)) {
$columns[$customAlias] = $columnAlias;
} else {
$columns[] = $columnAlias;
}
}
}
return $columns;
}
/**
* Filter this query using the given column and value
*
* This notifies the repository about the required filter column.
*
* @param string $column
* @param mixed $value
*
* @return $this
*/
public function where($column, $value = null)
{
$this->query->where(
$this->repository->requireFilterColumn($this->target, $column, $this),
$this->repository->persistColumn($this->target, $column, $value)
);
return $this;
}
/**
* Add an additional filter expression to this query
*
* This notifies the repository about each required filter column.
*
* @param Filter $filter
*
* @return $this
*/
public function applyFilter(Filter $filter)
{
return $this->addFilter($filter);
}
/**
* Set a filter for this query
*
* This notifies the repository about each required filter column.
*
* @param Filter $filter
*
* @return $this
*/
public function setFilter(Filter $filter)
{
$this->query->setFilter($this->repository->requireFilter($this->target, $filter, $this));
return $this;
}
/**
* Add an additional filter expression to this query
*
* This notifies the repository about each required filter column.
*
* @param Filter $filter
*
* @return $this
*/
public function addFilter(Filter $filter)
{
$this->query->addFilter($this->repository->requireFilter($this->target, $filter, $this));
return $this;
}
/**
* Return the current filter
*
* @return Filter
*/
public function getFilter()
{
return $this->query->getFilter();
}
/**
* Add a sort rule for this query
*
* If called without a specific column, the repository's defaul sort rules will be applied.
* This notifies the repository about each column being required as filter column.
*
* @param string $field The name of the column by which to sort the query's result
* @param string $direction The direction to use when sorting (asc or desc, default is asc)
*
* @return $this
*/
public function order($field = null, $direction = null)
{
$sortRules = $this->repository->getSortRules();
if ($field === null) {
// Use first available sort rule as default
if (empty($sortRules)) {
// Return early in case of no sort defaults and no given $field
return $this;
}
$sortColumns = reset($sortRules);
if (! array_key_exists('columns', $sortColumns)) {
$sortColumns['columns'] = array(key($sortRules));
}
if ($direction !== null || !array_key_exists('order', $sortColumns)) {
$sortColumns['order'] = $direction ?: static::SORT_ASC;
}
} elseif (array_key_exists($field, $sortRules)) {
$sortColumns = $sortRules[$field];
if (! array_key_exists('columns', $sortColumns)) {
$sortColumns['columns'] = array($field);
}
if ($direction !== null || !array_key_exists('order', $sortColumns)) {
$sortColumns['order'] = $direction ?: static::SORT_ASC;
}
} else {
$sortColumns = array(
'columns' => array($field),
'order' => $direction
);
};
$baseDirection = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC;
foreach ($sortColumns['columns'] as $column) {
list($column, $specificDirection) = $this->splitOrder($column);
try {
$this->query->order(
$this->repository->requireFilterColumn($this->target, $column, $this),
$specificDirection ?: $baseDirection
// I would have liked the following solution, but hey, a coder should be allowed to produce crap...
// $specificDirection && (! $direction || $column !== $field) ? $specificDirection : $baseDirection
);
} catch (QueryException $_) {
Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName());
}
}
return $this;
}
/**
* Extract and return the name and direction of the given sort column definition
*
* @param string $field
*
* @return array An array of two items: $columnName, $direction
*/
protected function splitOrder($field)
{
$columnAndDirection = explode(' ', $field, 2);
if (count($columnAndDirection) === 1) {
$column = $field;
$direction = null;
} else {
$column = $columnAndDirection[0];
$direction = strtoupper($columnAndDirection[1]) === static::SORT_DESC
? static::SORT_DESC
: static::SORT_ASC;
}
return array($column, $direction);
}
/**
* Return whether any sort rules were applied to this query
*
* @return bool
*/
public function hasOrder()
{
return $this->query->hasOrder();
}
/**
* Return the sort rules applied to this query
*
* @return array
*/
public function getOrder()
{
return $this->query->getOrder();
}
/**
* Limit this query's results
*
* @param int $count When to stop returning results
* @param int $offset When to start returning results
*
* @return $this
*/
public function limit($count = null, $offset = null)
{
$this->query->limit($count, $offset);
return $this;
}
/**
* Return whether this query does not return all available entries from its result
*
* @return bool
*/
public function hasLimit()
{
return $this->query->hasLimit();
}
/**
* Return the limit when to stop returning results
*
* @return int
*/
public function getLimit()
{
return $this->query->getLimit();
}
/**
* Return whether this query does not start returning results at the very first entry
*
* @return bool
*/
public function hasOffset()
{
return $this->query->hasOffset();
}
/**
* Return the offset when to start returning results
*
* @return int
*/
public function getOffset()
{
return $this->query->getOffset();
}
/**
* Fetch and return the first column of this query's first row
*
* @return mixed|false False in case of no result
*/
public function fetchOne()
{
if (! $this->hasOrder()) {
$this->order();
}
$result = $this->query->fetchOne();
if ($result !== false && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns();
$column = isset($columns[0]) ? $columns[0] : key($columns);
return $this->repository->retrieveColumn($this->target, $column, $result);
}
return $result;
}
/**
* Fetch and return the first row of this query's result
*
* @return object|false False in case of no result
*/
public function fetchRow()
{
if (! $this->hasOrder()) {
$this->order();
}
$result = $this->query->fetchRow();
if ($result !== false && $this->repository->providesValueConversion($this->target)) {
foreach ($this->getColumns() as $alias => $column) {
if (! is_string($alias)) {
$alias = $column;
}
$result->$alias = $this->repository->retrieveColumn($this->target, $alias, $result->$alias);
}
}
return $result;
}
/**
* Fetch and return the first column of all rows of the result set as an array
*
* @return array
*/
public function fetchColumn()
{
if (! $this->hasOrder()) {
$this->order();
}
$results = $this->query->fetchColumn();
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns();
$aliases = array_keys($columns);
$column = is_int($aliases[0]) ? $columns[0] : $aliases[0];
foreach ($results as & $value) {
$value = $this->repository->retrieveColumn($this->target, $column, $value);
}
}
return $results;
}
/**
* Fetch and return all rows of this query's result set as an array of key-value pairs
*
* The first column is the key, the second column is the value.
*
* @return array
*/
public function fetchPairs()
{
if (! $this->hasOrder()) {
$this->order();
}
$results = $this->query->fetchPairs();
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns();
$aliases = array_keys($columns);
$newResults = array();
foreach ($results as $colOneValue => $colTwoValue) {
$colOne = $aliases[0] !== 0 ? $aliases[0] : $columns[0];
$colTwo = count($aliases) < 2 ? $colOne : ($aliases[1] !== 1 ? $aliases[1] : $columns[1]);
$colOneValue = $this->repository->retrieveColumn($this->target, $colOne, $colOneValue);
$newResults[$colOneValue] = $this->repository->retrieveColumn($this->target, $colTwo, $colTwoValue);
}
$results = $newResults;
}
return $results;
}
/**
* Fetch and return all results of this query
*
* @return array
*/
public function fetchAll()
{
if (! $this->hasOrder()) {
$this->order();
}
$results = $this->query->fetchAll();
if (! empty($results) && $this->repository->providesValueConversion($this->target)) {
$columns = $this->getColumns();
foreach ($results as $row) {
foreach ($columns as $alias => $column) {
if (! is_string($alias)) {
$alias = $column;
}
$row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias);
}
}
}
return $results;
}
/**
* Count all results of this query
*
* @return int
*/
public function count()
{
return $this->query->count();
}
/**
* Start or rewind the iteration
*/
public function rewind()
{
if ($this->iterator === null) {
if (! $this->hasOrder()) {
$this->order();
}
$iterator = $this->repository->getDataSource()->query($this->query);
if ($iterator instanceof IteratorAggregate) {
$this->iterator = $iterator->getIterator();
} else {
$this->iterator = $iterator;
}
}
$this->iterator->rewind();
Benchmark::measure('Query result iteration started');
}
/**
* Fetch and return the current row of this query's result
*
* @return object
*/
public function current()
{
$row = $this->iterator->current();
if ($this->repository->providesValueConversion($this->target)) {
foreach ($this->getColumns() as $alias => $column) {
if (! is_string($alias)) {
$alias = $column;
}
$row->$alias = $this->repository->retrieveColumn($this->target, $alias, $row->$alias);
}
}
return $row;
}
/**
* Return whether the current row of this query's result is valid
*
* @return bool
*/
public function valid()
{
if (! $this->iterator->valid()) {
Benchmark::measure('Query result iteration finished');
return false;
}
return true;
}
/**
* Return the key for the current row of this query's result
*
* @return mixed
*/
public function key()
{
return $this->iterator->key();
}
/**
* Advance to the next row of this query's result
*/
public function next()
{
$this->iterator->next();
}
}

View File

@ -426,7 +426,7 @@ class User
// matches
$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

View File

@ -0,0 +1,198 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Controller;
use \Zend_Controller_Action_Exception;
use Icinga\Application\Config;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\UserBackendInterface;
use Icinga\Authentication\UserGroup\UserGroupBackend;
use Icinga\Authentication\UserGroup\UserGroupBackendInterface;
use Icinga\Security\SecurityException;
use Icinga\Web\Controller;
/**
* Base class for authentication backend controllers
*/
class AuthBackendController extends Controller
{
/**
* Redirect to the first permitted list action
*/
final public function indexAction()
{
if ($this->hasPermission('config/authentication/users/show')) {
$this->redirectNow('user/list');
} elseif ($this->hasPermission('config/authentication/groups/show')) {
$this->redirectNow('group/list');
} elseif ($this->hasPermission('config/authentication/roles/show')) {
$this->redirectNow('role/list');
} else {
throw new SecurityException($this->translate('No permission for authentication configuration'));
}
}
/**
* Return all user backends implementing the given interface
*
* @param string $interface The class path of the interface, or null if no interface check should be made
*
* @return array
*/
protected function loadUserBackends($interface = null)
{
$backends = array();
foreach (Config::app('authentication') as $backendName => $backendConfig) {
$candidate = UserBackend::create($backendName, $backendConfig);
if (! $interface || $candidate instanceof $interface) {
$backends[] = $candidate;
}
}
return $backends;
}
/**
* Return the given user backend or the first match in order
*
* @param string $name The name of the backend, or null in case the first match should be returned
* @param string $interface The interface the backend should implement, no interface check if null
*
* @return UserBackendInterface
*
* @throws Zend_Controller_Action_Exception In case the given backend name is invalid
*/
protected function getUserBackend($name = null, $interface = 'Icinga\Data\Selectable')
{
if ($name !== null) {
$config = Config::app('authentication');
if (! $config->hasSection($name)) {
$this->httpNotFound(sprintf($this->translate('Authentication backend "%s" not found'), $name));
} else {
$backend = UserBackend::create($name, $config->getSection($name));
if ($interface && !$backend instanceof $interface) {
$interfaceParts = explode('\\', strtolower($interface));
throw new Zend_Controller_Action_Exception(
sprintf(
$this->translate('Authentication backend "%s" is not %s'),
$name,
array_pop($interfaceParts)
),
400
);
}
}
} else {
$backends = $this->loadUserBackends($interface);
$backend = array_shift($backends);
}
return $backend;
}
/**
* Return all user group backends implementing the given interface
*
* @param string $interface The class path of the interface, or null if no interface check should be made
*
* @return array
*/
protected function loadUserGroupBackends($interface = null)
{
$backends = array();
foreach (Config::app('groups') as $backendName => $backendConfig) {
$candidate = UserGroupBackend::create($backendName, $backendConfig);
if (! $interface || $candidate instanceof $interface) {
$backends[] = $candidate;
}
}
return $backends;
}
/**
* Return the given user group backend or the first match in order
*
* @param string $name The name of the backend, or null in case the first match should be returned
* @param string $interface The interface the backend should implement, no interface check if null
*
* @return UserGroupBackendInterface
*
* @throws Zend_Controller_Action_Exception In case the given backend name is invalid
*/
protected function getUserGroupBackend($name = null, $interface = 'Icinga\Data\Selectable')
{
if ($name !== null) {
$config = Config::app('groups');
if (! $config->hasSection($name)) {
$this->httpNotFound(sprintf($this->translate('User group backend "%s" not found'), $name));
} else {
$backend = UserGroupBackend::create($name, $config->getSection($name));
if ($interface && !$backend instanceof $interface) {
$interfaceParts = explode('\\', strtolower($interface));
throw new Zend_Controller_Action_Exception(
sprintf(
$this->translate('User group backend "%s" is not %s'),
$name,
array_pop($interfaceParts)
),
400
);
}
}
} else {
$backends = $this->loadUserGroupBackends($interface);
$backend = array_shift($backends);
}
return $backend;
}
/**
* Create the tabs to list users and groups
*/
protected function createListTabs()
{
$tabs = $this->getTabs();
if ($this->hasPermission('config/authentication/users/show')) {
$tabs->add(
'user/list',
array(
'title' => $this->translate('List users of authentication backends'),
'label' => $this->translate('Users'),
'icon' => 'user',
'url' => 'user/list'
)
);
}
if ($this->hasPermission('config/authentication/groups/show')) {
$tabs->add(
'group/list',
array(
'title' => $this->translate('List groups of user group backends'),
'label' => $this->translate('Groups'),
'icon' => 'users',
'url' => 'group/list'
)
);
}
if ($this->hasPermission('config/authentication/roles/show')) {
$tabs->add(
'role/list',
array(
'title' => $this->translate(
'Configure roles to permit or restrict users and groups accessing Icinga Web 2'
),
'label' => $this->translate('Roles'),
'url' => 'role/list'
)
);
}
return $tabs;
}
}

View File

@ -39,7 +39,7 @@ class ErrorLabeller extends Zend_Translate_Adapter
protected function createMessages($element)
{
$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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,14 +60,6 @@ class ConfigObjectTest extends BaseTestCase
);
}
public function testWhetherConfigObjectsAreCountable()
{
$config = new ConfigObject(array('a' => 'b', 'c' => array('d' => 'e')));
$this->assertInstanceOf('Countable', $config, 'ConfigObject objects do not implement interface `Countable\'');
$this->assertEquals(2, count($config), 'ConfigObject objects do not count properties and sections correctly');
}
public function testWhetherConfigObjectsAreTraversable()
{
$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();

View File

@ -1,258 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
// We need to overwrite the library functions in the regular namespace to mock library functions. We run
// this test in a separate process to not alter different test cases.
namespace Icinga\Protocol\Ldap;
use Icinga\Test\BaseTestCase;
use Icinga\Data\ConfigObject;
use Mockery;
/**
* @runTestsInSeparateProcesses
*/
class ConnectionTest extends BaseTestCase
{
private $connection;
public $pagedResultsCalled;
public $startTlsCalled;
public $activatedOptions;
private function mockLdapFunctions () {
global $self;
$self = $this;
function ldap_connect()
{
return true;
}
function ldap_bind()
{
return true;
}
function ldap_search ()
{
return true;
}
function ldap_get_entries()
{
return 1;
}
function ldap_count_entries()
{
return 1;
}
function ldap_read()
{
return true;
}
function ldap_first_entry($ds, $result)
{
return $result;
}
function ldap_get_attributes()
{
global $self;
return $self->getAttributesMock;
}
function ldap_start_tls()
{
global $self;
$self->startTlsCalled = true;
}
function ldap_set_option($ds, $option, $value)
{
global $self;
$self->activatedOptions[$option] = $value;
return true;
}
function ldap_set($ds, $option)
{
global $self;
$self->activatedOptions[] = $option;
}
function ldap_control_paged_result()
{
global $self;
$self->pagedResultsCalled = true;
return true;
}
function ldap_control_paged_result_response()
{
return true;
}
function ldap_get_dn()
{
return NULL;
}
function ldap_free_result()
{
return NULL;
}
}
private function node(&$element, $name)
{
$element['count']++;
$element[$name] = array('count' => 0);
$element[] = $name;
}
private function addEntry(&$element, $name, $entry)
{
$element[$name]['count']++;
$element[$name][] = $entry;
}
private function mockQuery()
{
return Mockery::mock('overload:Icinga\Protocol\Ldap\Query')
->shouldReceive(array(
'from' => Mockery::self(),
'create' => array('count' => 1),
'listFields' => array('count' => 1),
'getLimit' => 1,
'hasOffset' => false,
'hasBase' => false,
'getSortColumns' => array(),
'getUsePagedResults' => true
));
}
private function connectionFetchAll()
{
$this->mockQuery();
$this->connection->connect();
$this->connection->fetchAll(Mockery::self());
}
public function setUp()
{
$this->pagedResultsCalled = false;
$this->startTlsCalled = false;
$this->activatedOptions = array();
$this->mockLdapFunctions();
$config = new ConfigObject(
array(
'hostname' => 'localhost',
'root_dn' => 'dc=example,dc=com',
'bind_dn' => 'cn=user,ou=users,dc=example,dc=com',
'bind_pw' => '***'
)
);
$this->connection = new Connection($config);
$caps = array('count' => 0);
$this->node($caps, 'defaultNamingContext');
$this->node($caps, 'namingContexts');
$this->node($caps, 'supportedCapabilities');
$this->node($caps, 'supportedControl');
$this->node($caps, 'supportedLDAPVersion');
$this->node($caps, 'supportedExtension');
$this->getAttributesMock = $caps;
}
public function testUsePageControlWhenAnnounced()
{
if (version_compare(PHP_VERSION, '5.4.0') < 0) {
$this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0');
}
$this->addEntry($this->getAttributesMock, 'supportedControl', Capability::LDAP_PAGED_RESULT_OID_STRING);
$this->connectionFetchAll();
// see ticket #7993
$this->assertEquals(true, $this->pagedResultsCalled, "Use paged result when capability is present.");
}
public function testDontUsePagecontrolWhenNotAnnounced()
{
if (version_compare(PHP_VERSION, '5.4.0') < 0) {
$this->markTestSkipped('Page control needs at least PHP_VERSION 5.4.0');
}
$this->connectionFetchAll();
// see ticket #8490
$this->assertEquals(false, $this->pagedResultsCalled, "Don't use paged result when capability is not announced.");
}
public function testUseLdapV2WhenAnnounced()
{
// TODO: Test turned off, see other TODO in Ldap/Connection.
$this->markTestSkipped('LdapV2 currently turned off.');
$this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 2);
$this->connectionFetchAll();
$this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
$this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 2);
}
public function testUseLdapV3WhenAnnounced()
{
$this->addEntry($this->getAttributesMock, 'supportedLDAPVersion', 3);
$this->connectionFetchAll();
$this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
$this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active");
}
public function testDefaultSettings()
{
$this->connectionFetchAll();
$this->assertArrayHasKey(LDAP_OPT_PROTOCOL_VERSION, $this->activatedOptions, "LDAP version must be set");
$this->assertEquals($this->activatedOptions[LDAP_OPT_PROTOCOL_VERSION], 3, "LDAPv3 must be active");
$this->assertArrayHasKey(LDAP_OPT_REFERRALS, $this->activatedOptions, "Following referrals must be turned off");
$this->assertEquals($this->activatedOptions[LDAP_OPT_REFERRALS], 0, "Following referrals must be turned off");
}
public function testActiveDirectoryDiscovery()
{
$this->addEntry($this->getAttributesMock, 'supportedCapabilities', Capability::LDAP_CAP_ACTIVE_DIRECTORY_OID);
$this->connectionFetchAll();
$this->assertEquals(true, $this->connection->getCapabilities()->hasAdOid(),
"Server with LDAP_CAP_ACTIVE_DIRECTORY_OID must be recognized as Active Directory.");
}
public function testDefaultNamingContext()
{
$this->addEntry($this->getAttributesMock, 'defaultNamingContext', 'dn=default,dn=contex');
$this->connectionFetchAll();
$this->assertEquals('dn=default,dn=contex', $this->connection->getCapabilities()->getDefaultNamingContext(),
'Default naming context must be correctly recognized.');
}
public function testDefaultNamingContextFallback()
{
$this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=some,dn=other,dn=context');
$this->addEntry($this->getAttributesMock, 'namingContexts', 'dn=default,dn=context');
$this->connectionFetchAll();
$this->assertEquals('dn=some,dn=other,dn=context', $this->connection->getCapabilities()->getDefaultNamingContext(),
'If defaultNamingContext is missing, the connection must fallback to first namingContext.');
}
}

View File

@ -36,51 +36,11 @@ class QueryTest extends BaseTestCase
return $select;
}
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);
}
}

View File

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