diff --git a/application/controllers/SuggestController.php b/application/controllers/SuggestController.php index 40076e66e..47d6370ed 100644 --- a/application/controllers/SuggestController.php +++ b/application/controllers/SuggestController.php @@ -2,6 +2,7 @@ namespace Icinga\Module\Director\Controllers; +use Icinga\Module\Director\Objects\IcingaUser; use Icinga\Module\Director\Restriction\HostgroupRestriction; use ipl\Html\Html; use Icinga\Exception\NotFoundError; @@ -26,7 +27,7 @@ public function indexAction() $key = null; if (strpos($context, '!') !== false) { - list($context, $key) = preg_split('~!~', $context, 2); + [$context, $key] = preg_split('~!~', $context, 2); } $func = 'suggest' . ucfirst($context); @@ -264,6 +265,14 @@ protected function suggestServiceFilterColumns() ]); } + protected function suggestUserFilterColumns() + { + return $this->getFilterColumns('user.', [ + $this->translate('User properties'), + $this->translate('Custom variables') + ]); + } + protected function suggestDataListValuesForListId($id) { $db = $this->db()->getDbAdapter(); @@ -336,6 +345,8 @@ protected function getFilterColumns($prefix, $keys) { if ($prefix === 'host.') { $all = IcingaHost::enumProperties($this->db(), $prefix); + } elseif ($prefix === 'user.') { + $all = IcingaUser::enumProperties($this->db(), $prefix); } else { $all = IcingaService::enumProperties($this->db(), $prefix); } diff --git a/application/controllers/UsergroupController.php b/application/controllers/UsergroupController.php index e58fd7e06..2e1360e35 100644 --- a/application/controllers/UsergroupController.php +++ b/application/controllers/UsergroupController.php @@ -2,8 +2,55 @@ namespace Icinga\Module\Director\Controllers; +use gipfl\Web\Widget\Hint; +use Icinga\Module\Director\Forms\IcingaDeleteUsergroupForm; use Icinga\Module\Director\Web\Controller\ObjectController; +use Icinga\Web\Notification; +use ipl\Web\Url; class UsergroupController extends ObjectController { + public function deleteAction() + { + $this->addTitle($this->translate('Delete Usergroup')); + + $directMemberQuery = $this->db() + ->select() + ->from('icinga_usergroup_user') + ->where('usergroup_id', $this->object->get('id')); + + $appliedMemberQuery = $this->db() + ->select() + ->from('icinga_usergroup_user_resolved') + ->where('usergroup_id', $this->object->get('id')); + + if ($this->db()->count($directMemberQuery) > 0 || $this->db()->count($appliedMemberQuery) > 0) { + $this->content()->add( + Hint::info(sprintf( + $this->translate('The usergroup "%s" has members. Do you still want to delete it?'), + $this->object->getObjectName() + )) + ); + } else { + $this->content()->add( + Hint::info(sprintf( + $this->translate('The usergroup "%s" does not have any members. You can go ahead and delete it.'), + $this->object->getObjectName() + )) + ); + } + + $this->content()->add( + (new IcingaDeleteUsergroupForm($this->object, $this->db(), $this->branch)) + ->on(IcingaDeleteUsergroupForm::ON_SUCCESS, function () { + Notification::success(sprintf( + $this->translate('User group %s has been deleted.'), + $this->object->getObjectName() + )); + + $this->redirectNow(Url::fromPath('director/usergroups')); + }) + ->handleRequest($this->getServerRequest()) + ); + } } diff --git a/application/forms/IcingaDeleteUsergroupForm.php b/application/forms/IcingaDeleteUsergroupForm.php new file mode 100644 index 000000000..7df111892 --- /dev/null +++ b/application/forms/IcingaDeleteUsergroupForm.php @@ -0,0 +1,37 @@ +addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); + + $this->addElement('submit', 'submit', [ + 'label' => $this->translate('Delete') + ]); + } + + protected function onSuccess(): void + { + (new DbObjectStore($this->db, $this->branch))->delete($this->object); + } +} diff --git a/application/forms/IcingaServiceForm.php b/application/forms/IcingaServiceForm.php index 3e21bc8cd..ba5c290f5 100644 --- a/application/forms/IcingaServiceForm.php +++ b/application/forms/IcingaServiceForm.php @@ -17,6 +17,7 @@ use Icinga\Module\Director\Web\Table\ObjectsTableHost; use ipl\Html\Html; use gipfl\IcingaWeb2\Link; +use ipl\Html\HtmlElement; use RuntimeException; class IcingaServiceForm extends DirectorObjectForm @@ -666,6 +667,15 @@ protected function addGroupsElement() )); } + $applied = $this->getAppliedGroups(); + if (! empty($applied)) { + $this->addElement('simpleNote', 'applied_groups', [ + 'label' => $this->translate('Applied groups'), + 'value' => $this->createServicegroupLinks($applied), + 'ignore' => true, + ]); + } + return $this; } @@ -808,4 +818,43 @@ protected function setDefaultNameFromTemplate($imports) } } } + + /** + * Create links to applied servicegroups. + * + * @param $groups + * + * @return HtmlElement + */ + protected function createServicegroupLinks($groups): HtmlElement + { + $links = []; + foreach ($groups as $name) { + if (! empty($links)) { + $links[] = ', '; + } + $links[] = Link::create( + $name, + 'director/servicegroup', + ['name' => $name], + ['data-base-target' => '_next'] + ); + } + + return Html::tag('span', ['class' => 'host-group-links'], $links); + } + + /** + * Get applied servicegroups. + * + * @return array + */ + protected function getAppliedGroups(): array + { + if ($this->isNew()) { + return []; + } + + return $this->object()->getAppliedGroups(); + } } diff --git a/application/forms/IcingaUserForm.php b/application/forms/IcingaUserForm.php index bff22525b..9828a3d65 100644 --- a/application/forms/IcingaUserForm.php +++ b/application/forms/IcingaUserForm.php @@ -2,7 +2,10 @@ namespace Icinga\Module\Director\Forms; +use gipfl\IcingaWeb2\Link; use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use ipl\Html\Html; +use ipl\Html\HtmlElement; class IcingaUserForm extends DirectorObjectForm { @@ -116,6 +119,15 @@ protected function addGroupsElement() ) )); + $applied = $this->getAppliedGroups(); + if (! empty($applied)) { + $this->addElement('simpleNote', 'applied_groups', [ + 'label' => $this->translate('Applied groups'), + 'value' => $this->createUsergroupLinks($applied), + 'ignore' => true, + ]); + } + return $this; } @@ -211,4 +223,43 @@ protected function enumUsergroups() return $db->fetchPairs($select); } + + /** + * Get applied user groups + * + * @return array + */ + protected function getAppliedGroups(): array + { + if ($this->isNew()) { + return []; + } + + return $this->object()->getAppliedGroups(); + } + + /** + * Create links for applied user groups + * + * @param $groups + * + * @return HtmlElement + */ + protected function createUsergroupLinks($groups): HtmlElement + { + $links = []; + foreach ($groups as $name) { + if (! empty($links)) { + $links[] = ', '; + } + $links[] = Link::create( + $name, + 'director/usergroup', + ['name' => $name], + ['data-base-target' => '_next'] + ); + } + + return Html::tag('span', ['class' => 'user-group-links'], $links); + } } diff --git a/application/forms/IcingaUserGroupForm.php b/application/forms/IcingaUserGroupForm.php index d9706b4e2..b867a4d03 100644 --- a/application/forms/IcingaUserGroupForm.php +++ b/application/forms/IcingaUserGroupForm.php @@ -3,6 +3,7 @@ namespace Icinga\Module\Director\Forms; use Icinga\Module\Director\Web\Form\DirectorObjectForm; +use ipl\Web\Url; class IcingaUserGroupForm extends DirectorObjectForm { @@ -20,6 +21,7 @@ public function setup() )); $this->addGroupDisplayNameElement() + ->addAssignmentElements() ->addZoneElements() ->groupMainProperties() ->setButtons(); @@ -44,4 +46,28 @@ protected function addZoneElements() return $this; } + + protected function addAssignmentElements() + { + $this->addAssignFilter([ + 'suggestionContext' => 'UserFilterColumns', + 'required' => false, + 'description' => $this->translate( + 'This allows you to configure an assignment filter. Please feel' + . ' free to combine as many nested operators as you want. The' + . ' "contains" operator is valid for arrays only. Please use' + . ' wildcards and the = (equals) operator when searching for' + . ' partial string matches, like in *.example.com' + ) + ]); + + return $this; + } + + protected function deleteObject($object) + { + $this->redirectAndExit( + Url::fromPath('director/usergroup/delete', ['uuid' => $object->getUniqueId()->toString()]) + ); + } } diff --git a/library/Director/Data/PropertiesFilter.php b/library/Director/Data/PropertiesFilter.php index a8c390602..01f5b39e1 100644 --- a/library/Director/Data/PropertiesFilter.php +++ b/library/Director/Data/PropertiesFilter.php @@ -8,6 +8,8 @@ class PropertiesFilter public static $HOST_PROPERTY = 'HOST_PROPERTY'; public static $SERVICE_PROPERTY = 'SERVICE_PROPERTY'; + public static $USER_PROPERTY = 'USER_PROPERTY'; + protected $blacklist = array( 'id', 'object_name', diff --git a/library/Director/Db.php b/library/Director/Db.php index 1cb625e91..e22b9e193 100644 --- a/library/Director/Db.php +++ b/library/Director/Db.php @@ -718,6 +718,31 @@ public function fetchDistinctServiceVars() return $this->db()->fetchAll($select); } + /** + * Fetch all distinct user vars. + * + * @return ?array + */ + public function fetchDistinctUserVars() + { + $select = $this->db()->select()->distinct()->from( + array('u' => 'icinga_user_var'), + array( + 'varname' => 'u.varname', + 'format' => 'u.format', + 'caption' => 'df.caption', + 'datatype' => 'df.datatype' + ) + )->joinLeft( + array('df' => 'director_datafield'), + 'df.varname = u.varname', + array() + )->order('varname'); + + return $this->db()->fetchAll($select); + } + + public function dbHexFunc($column) { if ($this->isPgsql()) { diff --git a/library/Director/Objects/IcingaObject.php b/library/Director/Objects/IcingaObject.php index 432730bc5..137483bb3 100644 --- a/library/Director/Objects/IcingaObject.php +++ b/library/Director/Objects/IcingaObject.php @@ -881,9 +881,6 @@ public function hasModifiedGroups() public function getAppliedGroups() { $this->assertGroupsSupport(); - if (! $this instanceof IcingaHost) { - throw new RuntimeException('getAppliedGroups is only available for hosts currently!'); - } if (! $this->hasBeenLoadedFromDb()) { // There are no stored related/resolved groups. We'll also not resolve // them here on demand. diff --git a/library/Director/Objects/IcingaUser.php b/library/Director/Objects/IcingaUser.php index 41002451b..c8b465fce 100644 --- a/library/Director/Objects/IcingaUser.php +++ b/library/Director/Objects/IcingaUser.php @@ -2,6 +2,8 @@ namespace Icinga\Module\Director\Objects; +use Icinga\Data\Db\DbConnection; +use Icinga\Module\Director\Data\PropertiesFilter; use Icinga\Module\Director\Db; use Icinga\Module\Director\DirectorObject\Automation\ExportInterface; use Icinga\Module\Director\Exception\DuplicateKeyException; @@ -48,8 +50,110 @@ class IcingaUser extends IcingaObject implements ExportInterface 'zone' => 'IcingaZone', ); + /** @var ?UserGroupMembershipResolver Resolver for user group memberships */ + protected $usergroupMembershipResolver; + public function getUniqueIdentifier() { return $this->getObjectName(); } + + protected function getUserGroupMembershipResolver() + { + if (! $this->usergroupMembershipResolver) { + $this->usergroupMembershipResolver = new UserGroupMembershipResolver( + $this->getConnection() + ); + } + + return $this->usergroupMembershipResolver; + } + + protected function notifyResolvers() + { + $resolver = $this->getUserGroupMembershipResolver(); + $resolver->addObject($this); + $resolver->refreshDb(); + + return $this; + } + + /** + * Enumerate properties for user objects + * + * @param ?DbConnection $connection + * @param $prefix + * @param $filter + * + * @return array + */ + public static function enumProperties(DbConnection $connection = null, $prefix = '', $filter = null): array + { + $userProperties = array(); + if ($filter === null) { + $filter = new PropertiesFilter(); + } + + $realProperties = array_merge(['templates'], static::create()->listProperties()); + sort($realProperties); + + if ($filter->match(PropertiesFilter::$USER_PROPERTY, 'name')) { + $userProperties[$prefix . 'name'] = 'name'; + } + + foreach ($realProperties as $prop) { + if (!$filter->match(PropertiesFilter::$USER_PROPERTY, $prop)) { + continue; + } + + if (substr($prop, -3) === '_id') { + if ($prop === 'template_choice_id') { + continue; + } + $prop = substr($prop, 0, -3); + } + + $userProperties[$prefix . $prop] = $prop; + } + + unset($userProperties[$prefix . 'uuid']); + unset($userProperties[$prefix . 'custom_endpoint_name']); + + $userVars = []; + + if ($connection instanceof Db) { + foreach ($connection->fetchDistinctUserVars() as $var) { + if ($filter->match(PropertiesFilter::$CUSTOM_PROPERTY, $var->varname, $var)) { + if ($var->datatype) { + $userVars[$prefix . 'vars.' . $var->varname] = sprintf( + '%s (%s)', + $var->varname, + $var->caption + ); + } else { + $userVars[$prefix . 'vars.' . $var->varname] = $var->varname; + } + } + } + } + + //$properties['vars.*'] = 'Other custom variable'; + ksort($userVars); + + + $props = mt('director', 'User properties'); + $vars = mt('director', 'Custom variables'); + + $properties = []; + if (! empty($userProperties)) { + $properties[$props] = $userProperties; + $properties[$props][$prefix . 'groups'] = 'Groups'; + } + + if (! empty($userVars)) { + $properties[$vars] = $userVars; + } + + return $properties; + } } diff --git a/library/Director/Objects/IcingaUserGroup.php b/library/Director/Objects/IcingaUserGroup.php index 656235a22..2d474e156 100644 --- a/library/Director/Objects/IcingaUserGroup.php +++ b/library/Director/Objects/IcingaUserGroup.php @@ -8,6 +8,9 @@ class IcingaUserGroup extends IcingaObjectGroup protected $uuidColumn = 'uuid'; + /** @var UserGroupMembershipResolver */ + protected $userGroupMembershipResolver; + protected $defaultProperties = [ 'id' => null, 'uuid' => null, @@ -16,6 +19,7 @@ class IcingaUserGroup extends IcingaObjectGroup 'disabled' => 'n', 'display_name' => null, 'zone_id' => null, + 'assign_filter' => null ]; protected $relations = [ @@ -26,4 +30,39 @@ protected function prefersGlobalZone() { return false; } + + public function supportsAssignments(): bool + { + return true; + } + + /** + * Set the membership resolver for the user group. + * + * @param UserGroupMembershipResolver $resolver + * + * @return $this + */ + public function setUserGroupMembershipResolver(UserGroupMembershipResolver $resolver): static + { + $this->userGroupMembershipResolver = $resolver; + + return $this; + } + + /** + * Get the membership resolver for the user group. + * + * @return UserGroupMembershipResolver + */ + protected function getMemberShipResolver(): UserGroupMembershipResolver + { + if ($this->userGroupMembershipResolver === null) { + $this->userGroupMembershipResolver = new UserGroupMembershipResolver( + $this->getConnection() + ); + } + + return $this->userGroupMembershipResolver; + } } diff --git a/library/Director/Objects/UserGroupMembershipResolver.php b/library/Director/Objects/UserGroupMembershipResolver.php new file mode 100644 index 000000000..0e7f2795c --- /dev/null +++ b/library/Director/Objects/UserGroupMembershipResolver.php @@ -0,0 +1,8 @@ +