diff --git a/application/controllers/GroupController.php b/application/controllers/GroupController.php index d18397cf496..534f4ce51a4 100644 --- a/application/controllers/GroupController.php +++ b/application/controllers/GroupController.php @@ -4,11 +4,13 @@ namespace Icinga\Controllers; use Exception; +use GuzzleHttp\Psr7\ServerRequest; use Icinga\Application\Logger; use Icinga\Authentication\User\DomainAwareInterface; use Icinga\Data\DataArray\ArrayDatasource; use Icinga\Data\Filter\Filter; use Icinga\Data\Reducible; +use Icinga\Data\UserSuggestions; use Icinga\Exception\NotFoundError; use Icinga\Forms\Config\UserGroup\AddMemberForm; use Icinga\Forms\Config\UserGroup\UserGroupForm; @@ -17,10 +19,11 @@ use Icinga\Web\Form; use Icinga\Web\Notification; use Icinga\Web\Url; -use Icinga\Web\Widget; +use ipl\Web\Url as IplUrl; class GroupController extends AuthBackendController { + protected $urlParams; public function init() { $this->view->title = $this->translate('User Groups'); @@ -225,23 +228,42 @@ public function addmemberAction() $this->assertPermission('config/access-control/groups'); $groupName = $this->params->getRequired('group'); $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); - - $form = new AddMemberForm(); - $form->setDataSource($this->fetchUsers()) + $form = (new AddMemberForm()) ->setBackend($backend) ->setGroupName($groupName) ->setRedirectUrl( Url::fromPath('group/show', array('backend' => $backend->getName(), 'group' => $groupName)) ) - ->setUidDisabled(); + ->setSuggestionUrl(IplUrl::fromPath( + 'group/complete', + [ + '_disableLayout' => true, + 'showCompact' => true, + 'backend' => $this->params->getRequired('backend'), + 'group' => $groupName + ] + )); - try { - $form->handleRequest(); - } catch (NotFoundError $_) { - $this->httpNotFound(sprintf($this->translate('Group "%s" not found'), $groupName)); - } + $form->on(AddMemberForm::ON_SUCCESS, function ($form) { + $this->getResponse()->redirectAndExit($form->getRedirectUrl()); + }); + $form->handleRequest(ServerRequest::fromGlobals()); + + $this->setTitle($this->translate('New User Group Member')); + $this->addContent($form); + } + + public function completeAction() + { + $backend = $this->getUserGroupBackend($this->params->getRequired('backend'), 'Icinga\Data\Extensible'); + $groupName = $this->params->getRequired('group'); - $this->renderForm($form, $this->translate('New User Group Member')); + $suggestions = new UserSuggestions(); + $suggestions->setUserGroupName($groupName); + $suggestions->setUserGroupBackend($backend); + $suggestions->setBackends($this->loadUserBackends('Icinga\Data\Selectable')); + $suggestions->forRequest(ServerRequest::fromGlobals()); + $this->getDocument()->add($suggestions); } /** diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php index debb9b7af00..072aa553aea 100644 --- a/application/forms/Config/UserGroup/AddMemberForm.php +++ b/application/forms/Config/UserGroup/AddMemberForm.php @@ -1,129 +1,39 @@ ds = $ds; - return $this; - } - - /** - * Set the user group backend to use - * - * @param Extensible $backend - * - * @return $this + * @param mixed $backend */ - public function setBackend(Extensible $backend) + public function setBackend($backend) { $this->backend = $backend; + return $this; } /** - * Set the group to add members for - * - * @param string $groupName - * - * @return $this + * @param mixed $groupName */ 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')); + return $this; } /** @@ -133,38 +43,33 @@ public function createElements(array $formData) */ 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' - )); + $q = $this->getValue($this->getSearchParameter()); + if (empty($q)) { + Notification::error(t('Please provide at least one username')); return false; } + $userNames = array_unique(explode(' ', $q)); + $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"'), + t('Failed to add "%s" as group member for "%s"'), $userName, $this->groupName )); - $this->error($e->getMessage()); + return false; } @@ -172,9 +77,9 @@ public function onSuccess() } if ($single) { - Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName)); + Notification::success(sprintf(t('Group member "%s" added successfully'), $userName)); } else { - Notification::success($this->translate('Group members added successfully')); + Notification::success(t('Group members added successfully')); } return true; diff --git a/library/Icinga/Data/SimpleSuggestions.php b/library/Icinga/Data/SimpleSuggestions.php index 0eb77cfcd74..6c1dd7dcf73 100644 --- a/library/Icinga/Data/SimpleSuggestions.php +++ b/library/Icinga/Data/SimpleSuggestions.php @@ -14,8 +14,7 @@ use Psr\Http\Message\ServerRequestInterface; use Traversable; - -abstract class SimpleSuggestions extends BaseHtmlElement +abstract class SimpleSuggestions extends BaseHtmlElement { const DEFAULT_LIMIT = 10; @@ -141,7 +140,7 @@ public function forRequest(ServerRequestInterface $request) $this->setData($this->fetchSuggestions($label)); - if($search) { + if ($search) { $this->setDefault($search); } @@ -158,5 +157,4 @@ public function renderUnwrapped() return parent::renderUnwrapped(); } - } diff --git a/library/Icinga/Data/UserSuggestions.php b/library/Icinga/Data/UserSuggestions.php new file mode 100644 index 00000000000..a4ec7a6037d --- /dev/null +++ b/library/Icinga/Data/UserSuggestions.php @@ -0,0 +1,119 @@ +userGroupBackend = $userGroupBackend; + + return $this; + } + + /** + * @param string $userGroupName + */ + public function setUserGroupName($userGroupName) + { + $this->userGroupName = $userGroupName; + + return $this; + } + + /** + * @return mixed + */ + public function getBackends() + { + return $this->backends; + } + + /** + * @param mixed $backends + */ + public function setBackends($backends) + { + $this->backends = $backends; + + return $this; + } + + /** + * @param $searchTerm + * + * @return \Generator|void + */ + protected function fetchSuggestions($searchTerm) + { + $count = 0; + foreach ($this->getBackends() as $backend) { + try { + if ($backend instanceof DomainAwareInterface) { + $domain = $backend->getDomain(); + } else { + $domain = null; + } + + if ($count === self::DEFAULT_LIMIT) { + return; + } + + $members = $this->userGroupBackend + ->select() + ->from('group_membership', ['user_name']) + ->where('group_name', $this->userGroupName) + ->fetchColumn(); + + $filter = Filter::matchAll( + Filter::where('user_name', $searchTerm), + Filter::not(Filter::where('user_name', $members)) + ); + + $users = $backend->select(['user_name'])->applyFilter($filter)->fetchColumn(); + + foreach ($users as $userName) { + $userObj = new User($userName); + if ($domain !== null) { + if ($userObj->hasDomain() && $userObj->getDomain() !== $domain) { + // Users listed in a user backend which is configured to be responsible for a domain should + // not have a domain in their username. Ultimately, if the username has a domain, it must + // not differ from the backend's domain. We could log here - but hey, who cares :) + continue; + } else { + $userObj->setDomain($domain); + } + } + + $count++; + + yield $userObj->getUsername(); + } + } catch (Exception $e) { + Logger::error($e); + Notification::warning(sprintf( + t('Failed to fetch any users from backend %s. Please check your log'), + $backend->getName() + )); + } + } + } +} diff --git a/public/css/icinga/forms.less b/public/css/icinga/forms.less index aa1087204fa..8c0b05f6770 100644 --- a/public/css/icinga/forms.less +++ b/public/css/icinga/forms.less @@ -582,3 +582,30 @@ form.icinga-form .form-info { } } } + +form.search-field { + max-width: 100%; + width: 100%; + + .term-container { + margin-top: 1em; + + input[type="button"] { + margin-right: 0.5em; + margin-bottom: 0.5em; + max-width: 120px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + background-color: var(--search-term-bg, @search-term-bg); + color: var(--search-term-color, @search-term-color); + + &:hover::before { + background-color: #f56; + + content: "\f00d"; //TODO Find solution + color: #f56; + } + } + } +}