Merge pull request #4336 from Icinga/feature/audit-view-3053

Audit View
This commit is contained in:
Johannes Meyer 2021-04-08 08:50:21 +02:00 committed by GitHub
commit 6f317ade30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1691 additions and 266 deletions

View File

@ -381,6 +381,16 @@ class GroupController extends AuthBackendController
'url' => 'role/list'
)
);
$tabs->add(
'role/audit',
[
'title' => $this->translate('Audit a user\'s or group\'s privileges'),
'label' => $this->translate('Audit'),
'url' => 'role/audit',
'baseTarget' => '_main',
]
);
}
if ($this->hasPermission('config/access-control/users')) {

View File

@ -3,11 +3,24 @@
namespace Icinga\Controllers;
use Exception;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Authentication\AdmissionLoader;
use Icinga\Authentication\Auth;
use Icinga\Authentication\RolesConfig;
use Icinga\Data\Selectable;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\Security\RoleForm;
use Icinga\Repository\Repository;
use Icinga\Security\SecurityException;
use Icinga\User;
use Icinga\Web\Controller\AuthBackendController;
use Icinga\Web\View\PrivilegeAudit;
use Icinga\Web\Widget\SingleValueSearchControl;
use ipl\Html\Html;
use ipl\Html\HtmlString;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
/**
* Manage user permissions and restrictions based on roles
@ -71,7 +84,7 @@ class RoleController extends AuthBackendController
$this->assertPermission('config/access-control/roles');
$role = new RoleForm();
$role->setRedirectUrl('role/list');
$role->setRedirectUrl('__CLOSE__');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Create Role'));
$role->add()->handleRequest();
@ -90,7 +103,7 @@ class RoleController extends AuthBackendController
$name = $this->params->getRequired('role');
$role = new RoleForm();
$role->setRedirectUrl('role/list');
$role->setRedirectUrl('__CLOSE__');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Update Role'));
$role->edit($name);
@ -113,7 +126,7 @@ class RoleController extends AuthBackendController
$name = $this->params->getRequired('role');
$role = new RoleForm();
$role->setRedirectUrl('role/list');
$role->setRedirectUrl('__CLOSE__');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Remove Role'));
$role->remove($name);
@ -127,6 +140,204 @@ class RoleController extends AuthBackendController
$this->renderForm($role, $this->translate('Remove Role'));
}
public function auditAction()
{
$this->assertPermission('config/access-control/roles');
$this->createListTabs()->activate('role/audit');
$this->view->title = t('Audit');
$roleName = $this->params->get('role');
$type = $this->params->has('group') ? 'group' : 'user';
$name = $this->params->get($type);
$backend = null;
if ($type === 'user') {
if ($name) {
$backend = $this->params->getRequired('backend');
} else {
$backends = $this->loadUserBackends();
if (! empty($backends)) {
$backend = array_shift($backends)->getName();
}
}
}
$form = new SingleValueSearchControl();
$form->setMetaDataNames('type', 'backend');
$form->populate(['q' => $name, 'q-type' => $type, 'q-backend' => $backend]);
$form->setInputLabel(t('Enter user or group name'));
$form->setSubmitLabel(t('Inspect'));
$form->setSuggestionUrl(Url::fromPath(
'role/suggest-role-member',
['_disableLayout' => true, 'showCompact' => true]
));
$form->on(SingleValueSearchControl::ON_SUCCESS, function ($form) {
$type = $form->getValue('q-type') ?: 'user';
$params = [$type => $form->getValue('q')];
if ($type === 'user') {
$params['backend'] = $form->getValue('q-backend');
}
$this->redirectNow(Url::fromPath('role/audit', $params));
})->handleRequest(ServerRequest::fromGlobals());
$this->addControl($form);
if (! $name) {
$this->addContent(Html::wantHtml(t('No user or group selected.')));
return;
}
if ($type === 'user') {
$header = Html::tag('h2', sprintf(t('Privilege Audit for User "%s"'), $name));
$user = new User($name);
$user->setAdditional('backend_name', $backend);
Auth::getInstance()->setupUser($user);
} else {
$header = Html::tag('h2', sprintf(t('Privilege Audit for Group "%s"'), $name));
$user = new User((string) time());
$user->setGroups([$name]);
(new AdmissionLoader())->applyRoles($user);
}
$chosenRole = null;
$assignedRoles = array_filter($user->getRoles(), function ($role) use ($user, &$chosenRole, $roleName) {
if (! in_array($role->getName(), $user->getAdditional('assigned_roles'), true)) {
return false;
}
if ($role->getName() === $roleName) {
$chosenRole = $role;
}
return true;
});
$this->addControl(Html::tag(
'ul',
['class' => 'privilege-audit-role-control'],
[
Html::tag('li', $roleName ? null : ['class' => 'active'], new Link(
t('All roles'),
Url::fromRequest()->without('role'),
['class' => 'button-link', 'title' => t('Show privileges of all roles')]
)),
array_map(function ($role) use ($roleName) {
return Html::tag(
'li',
$role->getName() === $roleName ? ['class' => 'active'] : null,
new Link(
$role->getName(),
Url::fromRequest()->setParam('role', $role->getName()),
[
'class' => 'button-link',
'title' => sprintf(t('Only show privileges of role %s'), $role->getName())
]
)
);
}, $assignedRoles)
]
));
$this->addControl($header);
$this->addContent(
(new PrivilegeAudit($chosenRole !== null ? [$chosenRole] : $assignedRoles))
->addAttributes(['id' => 'role-audit'])
);
}
public function suggestRoleMemberAction()
{
$this->assertHttpMethod('POST');
$requestData = $this->getRequest()->getPost();
$limit = $this->params->get('limit', 50);
$searchTerm = $requestData['term']['label'];
$userBackends = $this->loadUserBackends(Selectable::class);
$suggestions = [];
while ($limit > 0 && ! empty($userBackends)) {
/** @var Repository $backend */
$backend = array_shift($userBackends);
$query = $backend->select()
->from('user', ['user_name'])
->where('user_name', $searchTerm)
->limit($limit);
try {
$names = $query->fetchColumn();
} catch (Exception $e) {
continue;
}
$users = [];
foreach ($names as $name) {
$users[] = [$name, [
'type' => 'user',
'backend' => $backend->getName()
]];
}
if (! empty($users)) {
$suggestions[] = [
[
t('Users'),
HtmlString::create(' '),
Html::tag('span', ['class' => 'badge'], $backend->getName())
],
$users
];
}
$limit -= count($names);
}
$groupBackends = $this->loadUserGroupBackends(Selectable::class);
while ($limit > 0 && ! empty($groupBackends)) {
/** @var Repository $backend */
$backend = array_shift($groupBackends);
$query = $backend->select()
->from('group', ['group_name'])
->where('group_name', $searchTerm)
->limit($limit);
try {
$names = $query->fetchColumn();
} catch (Exception $e) {
continue;
}
$groups = [];
foreach ($names as $name) {
$groups[] = [$name, ['type' => 'group']];
}
if (! empty($groups)) {
$suggestions[] = [
[
t('Groups'),
HtmlString::create(' '),
Html::tag('span', ['class' => 'badge'], $backend->getName())
],
$groups
];
}
$limit -= count($names);
}
if (empty($suggestions)) {
$suggestions[] = [t('Your search does not match any user or group'), []];
}
$this->document->add(SingleValueSearchControl::createSuggestions($suggestions));
}
/**
* Create the tabs to display when listing roles
*/
@ -145,6 +356,16 @@ class RoleController extends AuthBackendController
)
);
$tabs->add(
'role/audit',
[
'title' => $this->translate('Audit a user\'s or group\'s privileges'),
'label' => $this->translate('Audit'),
'url' => 'role/audit',
'baseTarget' => '_main'
]
);
if ($this->hasPermission('config/access-control/users')) {
$tabs->add(
'user/list',

View File

@ -338,6 +338,16 @@ class UserController extends AuthBackendController
'url' => 'role/list'
)
);
$tabs->add(
'role/audit',
[
'title' => $this->translate('Audit a user\'s or group\'s privileges'),
'label' => $this->translate('Audit'),
'url' => 'role/audit',
'baseTarget' => '_main'
]
);
}
$tabs->add(

View File

@ -12,7 +12,6 @@ use Icinga\Forms\ConfigForm;
use Icinga\Forms\RepositoryForm;
use Icinga\Util\StringHelper;
use Icinga\Web\Notification;
use Zend_Form_Element;
/**
* Form for managing roles
@ -47,133 +46,7 @@ class RoleForm extends RepositoryForm
{
$this->setAttrib('class', self::DEFAULT_CLASSES . ' role-form');
$helper = new Zend_Form_Element('bogus');
$view = $this->getView();
$this->providedPermissions['application'] = [
$helper->filterName('application/announcements') => [
'name' => 'application/announcements',
'description' => $this->translate('Allow to manage announcements')
],
$helper->filterName('application/log') => [
'name' => 'application/log',
'description' => $this->translate('Allow to view the application log')
],
$helper->filterName('config/*') => [
'name' => 'config/*',
'description' => $this->translate('Allow full config access')
],
$helper->filterName('config/general') => [
'name' => 'config/general',
'description' => $this->translate('Allow to adjust the general configuration')
],
$helper->filterName('config/modules') => [
'name' => 'config/modules',
'description' => $this->translate('Allow to enable/disable and configure modules')
],
$helper->filterName('config/resources') => [
'name' => 'config/resources',
'description' => $this->translate('Allow to manage resources')
],
$helper->filterName('config/navigation') => [
'name' => 'config/navigation',
'description' => $this->translate('Allow to view and adjust shared navigation items')
],
$helper->filterName('config/access-control/*') => [
'name' => 'config/access-control/*',
'description' => $this->translate('Allow to fully manage access-control')
],
$helper->filterName('config/access-control/users') => [
'name' => 'config/access-control/users',
'description' => $this->translate('Allow to manage user accounts')
],
$helper->filterName('config/access-control/groups') => [
'name' => 'config/access-control/groups',
'description' => $this->translate('Allow to manage user groups')
],
$helper->filterName('config/access-control/roles') => [
'name' => 'config/access-control/roles',
'description' => $this->translate('Allow to manage roles')
],
$helper->filterName('user/*') => [
'name' => 'user/*',
'description' => $this->translate('Allow all account related functionalities')
],
$helper->filterName('user/password-change') => [
'name' => 'user/password-change',
'description' => $this->translate('Allow password changes in the account preferences')
],
$helper->filterName('user/application/stacktraces') => [
'name' => 'user/application/stacktraces',
'description' => $this->translate(
'Allow to adjust in the preferences whether to show stacktraces'
)
],
$helper->filterName('user/share/navigation') => [
'name' => 'user/share/navigation',
'description' => $this->translate('Allow to share navigation items')
]
];
$this->providedRestrictions['application'] = [
$helper->filterName('application/share/users') => [
'name' => 'application/share/users',
'description' => $this->translate(
'Restrict which users this role can share items and information with'
)
],
$helper->filterName('application/share/groups') => [
'name' => 'application/share/groups',
'description' => $this->translate(
'Restrict which groups this role can share items and information with'
)
]
];
$mm = Icinga::app()->getModuleManager();
foreach ($mm->listInstalledModules() as $moduleName) {
$modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
$this->providedPermissions[$moduleName][$helper->filterName($modulePermission)] = [
'isUsagePerm' => true,
'name' => $modulePermission,
'label' => $view->escape($this->translate('General Module Access')),
'description' => sprintf($this->translate('Allow access to module %s'), $moduleName)
];
$module = $mm->getModule($moduleName, false);
$permissions = $module->getProvidedPermissions();
$this->providedPermissions[$moduleName][$helper->filterName($moduleName . '/*')] = [
'isFullPerm' => true,
'name' => $moduleName . '/*',
'label' => $view->escape($this->translate('Full Module Access'))
];
foreach ($permissions as $permission) {
/** @var object $permission */
$this->providedPermissions[$moduleName][$helper->filterName($permission->name)] = [
'name' => $permission->name,
'label' => preg_replace(
'~^(\w+)(\/.*)~',
'<em>$1</em>$2',
$view->escape($permission->name)
),
'description' => $permission->description
];
}
foreach ($module->getProvidedRestrictions() as $restriction) {
$this->providedRestrictions[$moduleName][$helper->filterName($restriction->name)] = [
'name' => $restriction->name,
'label' => preg_replace(
'~^(\w+)(\/.*)~',
'<em>$1</em>$2',
$view->escape($restriction->name)
),
'description' => $restriction->description
];
}
}
list($this->providedPermissions, $this->providedRestrictions) = static::collectProvidedPrivileges();
}
protected function createFilter()
@ -273,14 +146,14 @@ class RoleForm extends RepositoryForm
$hasFullPerm = false;
foreach ($permissionList as $name => $spec) {
$elementName = $name;
$elementName = $this->filterName($name);
if ($hasFullPerm || $hasAdminPerm) {
$elementName .= '_fake';
}
$denyCheckbox = null;
if (! isset($spec['isFullPerm'])
&& substr($spec['name'], 0, strlen(self::DENY_PREFIX)) !== self::DENY_PREFIX
&& substr($name, 0, strlen(self::DENY_PREFIX)) !== self::DENY_PREFIX
) {
$denyCheckbox = $this->createElement('checkbox', self::DENY_PREFIX . $name, [
'decorators' => ['ViewHelper']
@ -298,13 +171,25 @@ class RoleForm extends RepositoryForm
'autosubmit' => isset($spec['isFullPerm']),
'disabled' => $hasFullPerm || $hasAdminPerm ?: null,
'value' => $hasFullPerm || $hasAdminPerm,
'label' => preg_replace(
// Adds a zero-width char after each slash to help browsers break onto newlines
'~(?<!<)/~',
'/&#8203;',
isset($spec['label']) ? $spec['label'] : $spec['name']
),
'description' => isset($spec['description']) ? $spec['description'] : $spec['name'],
'label' => isset($spec['label'])
? $spec['label']
: join('', iterator_to_array(call_user_func(function ($segments) {
foreach ($segments as $segment) {
if ($segment[0] === '/') {
// Adds a zero-width char after each slash to help browsers break onto newlines
yield '/&#8203;';
yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
} else {
yield '<em>' . $segment . '</em>';
}
}
}, preg_split(
'~(/[^/]+)~',
$name,
-1,
PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
)))),
'description' => isset($spec['description']) ? $spec['description'] : $name,
'decorators' => array_merge(
array_slice(self::$defaultElementDecorators, 0, 3),
[['Callback', ['callback' => function () use ($denyCheckbox) {
@ -320,11 +205,12 @@ class RoleForm extends RepositoryForm
if ($hasFullPerm || $hasAdminPerm) {
// Add a hidden element to preserve the configured permission value
$this->addElement('hidden', $name);
$this->addElement('hidden', $this->filterName($name));
}
if (isset($spec['isFullPerm'])) {
$hasFullPerm = isset($formData[$name]) && $formData[$name];
$filteredName = $this->filterName($name);
$hasFullPerm = isset($formData[$filteredName]) && $formData[$filteredName];
}
}
@ -336,23 +222,36 @@ class RoleForm extends RepositoryForm
]);
foreach ($this->providedRestrictions[$moduleName] as $name => $spec) {
$elements[] = $name;
$elementName = $this->filterName($name);
$elements[] = $elementName;
$this->addElement(
'text',
$name,
$elementName,
[
'label' => preg_replace(
// Adds a zero-width char after each slash to help browsers break onto newlines
'~(?<!<)/~',
'/&#8203;',
isset($spec['label']) ? $spec['label'] : $spec['name']
),
'label' => isset($spec['label'])
? $spec['label']
: join('', iterator_to_array(call_user_func(function ($segments) {
foreach ($segments as $segment) {
if ($segment[0] === '/') {
// Add zero-width char after each slash to help browsers break onto newlines
yield '/&#8203;';
yield '<span class="no-wrap">' . substr($segment, 1) . '</span>';
} else {
yield '<em>' . $segment . '</em>';
}
}
}, preg_split(
'~(/[^/]+)~',
$name,
-1,
PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
)))),
'description' => $spec['description'],
'style' => $isUnrestricted ? 'text-decoration:line-through;' : '',
'readonly' => $isUnrestricted ?: null
]
)
->getElement($name)
->getElement($elementName)
->getDecorator('Label')
->setOption('escape', false);
}
@ -402,11 +301,11 @@ class RoleForm extends RepositoryForm
foreach ($this->providedPermissions as $moduleName => $permissionList) {
foreach ($permissionList as $name => $spec) {
if (in_array($spec['name'], $permissions, true)) {
$values[$name] = 1;
if (in_array($name, $permissions, true)) {
$values[$this->filterName($name)] = 1;
}
if (in_array($spec['name'], $refusals, true)) {
if (in_array($name, $refusals, true)) {
$values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
}
}
@ -415,8 +314,8 @@ class RoleForm extends RepositoryForm
foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
foreach ($restrictionList as $name => $spec) {
if (isset($role->{$spec['name']})) {
$values[$name] = $role->{$spec['name']};
if (isset($role->$name)) {
$values[$this->filterName($name)] = $role->$name;
}
}
}
@ -430,9 +329,10 @@ class RoleForm extends RepositoryForm
foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
foreach ($restrictionList as $name => $spec) {
if (isset($values[$name])) {
$values[$spec['name']] = $values[$name];
unset($values[$name]);
$elementName = $this->filterName($name);
if (isset($values[$elementName])) {
$values[$name] = $values[$elementName];
unset($values[$elementName]);
}
}
}
@ -445,16 +345,17 @@ class RoleForm extends RepositoryForm
$refusals = [];
foreach ($this->providedPermissions as $moduleName => $permissionList) {
foreach ($permissionList as $name => $spec) {
if (isset($values[$name]) && $values[$name]) {
$permissions[] = $spec['name'];
$elementName = $this->filterName($name);
if (isset($values[$elementName]) && $values[$elementName]) {
$permissions[] = $name;
}
$denyName = $this->filterName(self::DENY_PREFIX . $name);
if (isset($values[$denyName]) && $values[$denyName]) {
$refusals[] = $spec['name'];
$refusals[] = $name;
}
unset($values[$name], $values[$denyName]);
unset($values[$elementName], $values[$denyName]);
}
}
@ -481,15 +382,15 @@ class RoleForm extends RepositoryForm
protected function sortPermissions(&$permissions)
{
return uasort($permissions, function ($a, $b) {
if (isset($a['isUsagePerm'])) {
return isset($b['isFullPerm']) ? 1 : -1;
} elseif (isset($b['isUsagePerm'])) {
return isset($a['isFullPerm']) ? -1 : 1;
return uksort($permissions, function ($a, $b) use ($permissions) {
if (isset($permissions[$a]['isUsagePerm'])) {
return isset($permissions[$b]['isFullPerm']) ? 1 : -1;
} elseif (isset($permissions[$b]['isUsagePerm'])) {
return isset($permissions[$a]['isFullPerm']) ? -1 : 1;
}
$aParts = explode('/', $a['name']);
$bParts = explode('/', $b['name']);
$aParts = explode('/', $a);
$bParts = explode('/', $b);
do {
$a = array_shift($aParts);
@ -566,4 +467,102 @@ class RoleForm extends RepositoryForm
));
}
}
/**
* Collect permissions and restrictions provided by Icinga Web 2 and modules
*
* @return array[$permissions, $restrictions]
*/
public static function collectProvidedPrivileges()
{
$providedPermissions['application'] = [
'application/announcements' => [
'description' => t('Allow to manage announcements')
],
'application/log' => [
'description' => t('Allow to view the application log')
],
'config/*' => [
'description' => t('Allow full config access')
],
'config/general' => [
'description' => t('Allow to adjust the general configuration')
],
'config/modules' => [
'description' => t('Allow to enable/disable and configure modules')
],
'config/resources' => [
'description' => t('Allow to manage resources')
],
'config/navigation' => [
'description' => t('Allow to view and adjust shared navigation items')
],
'config/access-control/*' => [
'description' => t('Allow to fully manage access-control')
],
'config/access-control/users' => [
'description' => t('Allow to manage user accounts')
],
'config/access-control/groups' => [
'description' => t('Allow to manage user groups')
],
'config/access-control/roles' => [
'description' => t('Allow to manage roles')
],
'user/*' => [
'description' => t('Allow all account related functionalities')
],
'user/password-change' => [
'description' => t('Allow password changes in the account preferences')
],
'user/application/stacktraces' => [
'description' => t('Allow to adjust in the preferences whether to show stacktraces')
],
'user/share/navigation' => [
'description' => t('Allow to share navigation items')
]
];
$providedRestrictions['application'] = [
'application/share/users' => [
'description' => t('Restrict which users this role can share items and information with')
],
'application/share/groups' => [
'description' => t('Restrict which groups this role can share items and information with')
]
];
$mm = Icinga::app()->getModuleManager();
foreach ($mm->listInstalledModules() as $moduleName) {
$modulePermission = Manager::MODULE_PERMISSION_NS . $moduleName;
$providedPermissions[$moduleName][$modulePermission] = [
'isUsagePerm' => true,
'label' => t('General Module Access'),
'description' => sprintf(t('Allow access to module %s'), $moduleName)
];
$module = $mm->getModule($moduleName, false);
$permissions = $module->getProvidedPermissions();
$providedPermissions[$moduleName][$moduleName . '/*'] = [
'isFullPerm' => true,
'label' => t('Full Module Access')
];
foreach ($permissions as $permission) {
/** @var object $permission */
$providedPermissions[$moduleName][$permission->name] = [
'description' => $permission->description
];
}
foreach ($module->getProvidedRestrictions() as $restriction) {
$providedRestrictions[$moduleName][$restriction->name] = [
'description' => $restriction->description
];
}
}
return [$providedPermissions, $providedRestrictions];
}
}

View File

@ -179,9 +179,15 @@ class AdmissionLoader
$roles = [];
$permissions = [];
$restrictions = [];
$assignedRoles = [];
$isUnrestricted = false;
foreach ($this->roleConfig as $roleName => $roleConfig) {
if (! isset($roles[$roleName]) && $this->match($username, $userGroups, $roleConfig)) {
$assigned = $this->match($username, $userGroups, $roleConfig);
if ($assigned) {
$assignedRoles[] = $roleName;
}
if (! isset($roles[$roleName]) && $assigned) {
foreach ($this->loadRole($roleName, $roleConfig) as $role) {
/** @var Role $role */
$roles[$role->getName()] = $role;
@ -206,6 +212,8 @@ class AdmissionLoader
}
}
$user->setAdditional('assigned_roles', $assignedRoles);
$user->setIsUnrestricted($isUnrestricted);
$user->setRestrictions($isUnrestricted ? [] : $restrictions);
$user->setPermissions($permissions);

View File

@ -17,6 +17,7 @@ use Icinga\User;
use Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore;
use Icinga\Web\Session;
use Icinga\Web\StyleSheet;
class Auth
{
@ -98,97 +99,23 @@ class Auth
public function setAuthenticated(User $user, $persist = true)
{
$username = $user->getUsername();
try {
$config = Config::app();
} catch (NotReadableError $e) {
Logger::error(
new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown: %s',
$username,
$e
)
);
$config = new Config();
}
if ($config->get('global', 'config_backend', 'db') !== 'none') {
$preferencesConfig = new ConfigObject(array(
'store' => $config->get('global', 'config_backend', 'db'),
'resource' => $config->get('global', 'config_resource')
));
try {
$preferencesStore = PreferencesStore::create(
$preferencesConfig,
$user
);
$preferences = new Preferences($preferencesStore->load());
} catch (Exception $e) {
Logger::error(
new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown: %s',
$username,
$e
)
);
$preferences = new Preferences();
}
} else {
$preferences = new Preferences();
}
// TODO(el): Quick-fix for #10957. Only reload CSS if the theme changed.
$this->getResponse()->setReloadCss(true);
$user->setPreferences($preferences);
$groups = $user->getGroups();
$userBackendName = $user->getAdditional('backend_name');
foreach (Config::app('groups') as $name => $config) {
$groupsUserBackend = $config->user_backend;
if ($groupsUserBackend
&& $groupsUserBackend !== 'none'
&& $userBackendName !== null
&& $groupsUserBackend !== $userBackendName
) {
// Do not ask for Group membership if a specific User Backend
// has been assigned to that Group Backend, and the user has
// been authenticated by another User Backend
continue;
}
$this->setupUser($user);
try {
$groupBackend = UserGroupBackend::create($name, $config);
$groupsFromBackend = $groupBackend->getMemberships($user);
} catch (Exception $e) {
Logger::error(
'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s',
$username,
$name,
$e
);
continue;
// Reload CSS if the theme changed
$themingConfig = Icinga::app()->getConfig()->getSection('themes');
$userTheme = $user->getPreferences()->getValue('icingaweb', 'theme');
if (! (bool) $themingConfig->get('disabled', false) && $userTheme !== null) {
$defaultTheme = $themingConfig->get('default', StyleSheet::DEFAULT_THEME);
if ($userTheme !== $defaultTheme) {
$this->getResponse()->setReloadCss(true);
}
if (empty($groupsFromBackend)) {
Logger::debug(
'No groups found in backend "%s" which the user "%s" is a member of.',
$name,
$user->getUsername()
);
continue;
}
$groupsFromBackend = array_values($groupsFromBackend);
Logger::debug(
'Groups found in backend "%s" for user "%s": %s',
$name,
$user->getUsername(),
join(', ', $groupsFromBackend)
);
$groups = array_merge($groups, array_combine($groupsFromBackend, $groupsFromBackend));
}
$user->setGroups($groups);
$admissionLoader = new AdmissionLoader();
$admissionLoader->applyRoles($user);
$this->user = $user;
if ($persist) {
$this->persistCurrentUser();
}
AuditHook::logActivity('login', 'User logged in');
}
@ -409,4 +336,110 @@ class Auth
$this->user = null;
Session::getSession()->purge();
}
/**
* Setup the given user
*
* This loads preferences, groups and roles.
*
* @param User $user
*
* @return void
*/
public function setupUser(User $user)
{
// Load the user's preferences
try {
$config = Config::app();
} catch (NotReadableError $e) {
Logger::error(
new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown: %s',
$user->getUsername(),
$e
)
);
$config = new Config();
}
if ($config->get('global', 'config_backend', 'db') !== 'none') {
$preferencesConfig = new ConfigObject([
'store' => $config->get('global', 'config_backend', 'db'),
'resource' => $config->get('global', 'config_resource')
]);
try {
$preferencesStore = PreferencesStore::create($preferencesConfig, $user);
$preferences = new Preferences($preferencesStore->load());
} catch (Exception $e) {
Logger::error(
new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown: %s',
$user->getUsername(),
$e
)
);
$preferences = new Preferences();
}
} else {
$preferences = new Preferences();
}
$user->setPreferences($preferences);
// Load the user's groups
$groups = $user->getGroups();
$userBackendName = $user->getAdditional('backend_name');
foreach (Config::app('groups') as $name => $config) {
$groupsUserBackend = $config->user_backend;
if ($groupsUserBackend
&& $groupsUserBackend !== 'none'
&& $userBackendName !== null
&& $groupsUserBackend !== $userBackendName
) {
// Do not ask for Group membership if a specific User Backend
// has been assigned to that Group Backend, and the user has
// been authenticated by another User Backend
continue;
}
try {
$groupBackend = UserGroupBackend::create($name, $config);
$groupsFromBackend = $groupBackend->getMemberships($user);
} catch (Exception $e) {
Logger::error(
'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s',
$user->getUsername(),
$name,
$e
);
continue;
}
if (empty($groupsFromBackend)) {
Logger::debug(
'No groups found in backend "%s" which the user "%s" is a member of.',
$name,
$user->getUsername()
);
continue;
}
$groupsFromBackend = array_values($groupsFromBackend);
Logger::debug(
'Groups found in backend "%s" for user "%s": %s',
$name,
$user->getUsername(),
join(', ', $groupsFromBackend)
);
$groups = array_merge($groups, array_combine($groupsFromBackend, $groupsFromBackend));
}
$user->setGroups($groups);
// Load the user's roles
$admissionLoader = new AdmissionLoader();
$admissionLoader->applyRoles($user);
}
}

View File

@ -253,19 +253,20 @@ class Role
*
* @param string $permission
* @param bool $ignoreParent Only evaluate the role's own permissions
* @param bool $cascadeUpwards `false` if `foo/bar/*` and `foo/bar/raboof` should not match `foo/*`
*
* @return bool
*/
public function grants($permission, $ignoreParent = false)
public function grants($permission, $ignoreParent = false, $cascadeUpwards = true)
{
foreach ($this->permissions as $grantedPermission) {
if ($this->match($grantedPermission, $permission)) {
if ($this->match($grantedPermission, $permission, $cascadeUpwards)) {
return true;
}
}
if (! $ignoreParent && $this->getParent() !== null) {
return $this->getParent()->grants($permission);
return $this->getParent()->grants($permission, false, $cascadeUpwards);
}
return false;

View File

@ -3,19 +3,26 @@
namespace Icinga\Web\Controller;
use ipl\Web\Compat\CompatController;
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\Web\Controller;
/**
* Base class for authentication backend controllers
*/
class AuthBackendController extends Controller
class AuthBackendController extends CompatController
{
public function init()
{
parent::init();
$this->tabs->disableLegacyExtensions();
}
/**
* Redirect to this controller's list action
*/

View File

@ -51,7 +51,8 @@ class StyleSheet
'css/icinga/compat.less',
'css/icinga/print.less',
'css/icinga/responsive.less',
'css/icinga/modal.less'
'css/icinga/modal.less',
'css/icinga/audit.less'
];
/**

View File

@ -0,0 +1,545 @@
<?php
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Web\View;
use Icinga\Authentication\Role;
use Icinga\Forms\Security\RoleForm;
use Icinga\Util\StringHelper;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\HtmlString;
use ipl\Stdlib\Filter;
use ipl\Web\Common\BaseTarget;
use ipl\Web\Filter\QueryString;
use ipl\Web\Url;
use ipl\Web\Widget\Icon;
use ipl\Web\Widget\Link;
class PrivilegeAudit extends BaseHtmlElement
{
use BaseTarget;
/** @var string */
const UNRESTRICTED_PERMISSION = 'unrestricted';
protected $tag = 'ul';
protected $defaultAttributes = ['class' => 'privilege-audit'];
/** @var Role[] */
protected $roles;
public function __construct(array $roles)
{
$this->roles = $roles;
$this->setBaseTarget('_next');
}
protected function auditPermission($permission)
{
$grantedBy = [];
$refusedBy = [];
foreach ($this->roles as $role) {
if ($permission === self::UNRESTRICTED_PERMISSION) {
if ($role->isUnrestricted()) {
$grantedBy[] = $role->getName();
}
} elseif ($role->denies($permission)) {
$refusedBy[] = $role->getName();
} elseif ($role->grants($permission, false, false)) {
$grantedBy[] = $role->getName();
}
}
$header = new HtmlElement('div');
if (! empty($refusedBy)) {
$header->add([
new Icon('times-circle', ['class' => 'refused']),
count($refusedBy) > 2
? sprintf(
tp(
'Refused by %s and %s as well as one other',
'Refused by %s and %s as well as %d others',
count($refusedBy) - 2
),
$refusedBy[0],
$refusedBy[1],
count($refusedBy) - 2
)
: sprintf(
tp('Refused by %s', 'Refused by %s and %s', count($refusedBy)),
...$refusedBy
)
]);
} elseif (! empty($grantedBy)) {
$header->add([
new Icon('check-circle', ['class' => 'granted']),
count($grantedBy) > 2
? sprintf(
tp(
'Granted by %s and %s as well as one other',
'Granted by %s and %s as well as %d others',
count($grantedBy) - 2
),
$grantedBy[0],
$grantedBy[1],
count($grantedBy) - 2
)
: sprintf(
tp('Granted by %s', 'Granted by %s and %s', count($grantedBy)),
...$grantedBy
)
]);
} else {
$header->add([new Icon('minus-circle'), t('Not granted or refused by any role')]);
}
$vClass = null;
$rolePaths = [];
foreach (array_reverse($this->roles) as $role) {
if (! in_array($role->getName(), $refusedBy, true) && ! in_array($role->getName(), $grantedBy, true)) {
continue;
}
/** @var Role[] $rolesReversed */
$rolesReversed = [];
do {
array_unshift($rolesReversed, $role);
} while (($role = $role->getParent()) !== null);
$path = new HtmlElement('ol');
$class = null;
$setInitiator = false;
foreach ($rolesReversed as $role) {
$granted = false;
$refused = false;
$icon = new Icon('minus-circle');
if ($permission === self::UNRESTRICTED_PERMISSION) {
if ($role->isUnrestricted()) {
$granted = true;
$icon = new Icon('check-circle', ['class' => 'granted']);
}
} elseif ($role->denies($permission, true)) {
$refused = true;
$icon = new Icon('times-circle', ['class' => 'refused']);
} elseif ($role->grants($permission, true, false)) {
$granted = true;
$icon = new Icon('check-circle', ['class' => 'granted']);
}
$connector = null;
if ($role->getParent() !== null) {
$connector = new HtmlElement('li', ['class' => ['connector', $class]]);
if ($setInitiator) {
$setInitiator = false;
$connector->getAttributes()->add('class', 'initiator');
}
$path->prepend($connector);
}
$path->prepend(new HtmlElement('li', [
'class' => ['role', $class],
'title' => $role->getName()
], new Link([$icon, $role->getName()], Url::fromPath('role/edit', ['role' => $role->getName()]))));
if ($refused) {
$setInitiator = $class !== 'refused';
$class = 'refused';
} elseif ($granted) {
$setInitiator = $class === null;
$class = $class ?: 'granted';
}
}
if ($vClass === null || $vClass === 'granted') {
$vClass = $class;
}
array_unshift($rolePaths, $path->prepend([
empty($rolePaths) ? null : new HtmlElement('li', ['class' => ['vertical-line', $vClass]]),
new HtmlElement('li', ['class' => [
'connector',
$class,
$setInitiator ? 'initiator' : null
]])
]));
}
return [
empty($refusedBy) ? (empty($grantedBy) ? null : true) : false,
new HtmlElement('div', [
'class' => [empty($rolePaths) ? null : 'collapsible', 'inheritance-paths'],
'data-toggle-element' => '.collapsible-control',
'data-no-persistence' => true,
'data-visible-height' => 0
], [
empty($rolePaths) ? $header : $header->addAttributes(['class' => 'collapsible-control']),
$rolePaths
])
];
}
protected function auditRestriction($restriction)
{
$restrictedBy = [];
$restrictions = [];
foreach ($this->roles as $role) {
if ($role->isUnrestricted()) {
$restrictedBy = [];
$restrictions = [];
break;
}
foreach ($this->collectRestrictions($role, $restriction) as $role => $roleRestriction) {
$restrictedBy[] = $role;
$restrictions[] = $roleRestriction;
}
}
$header = new HtmlElement('div');
if (! empty($restrictedBy)) {
$header->add([
new Icon('filter', ['class' => 'restricted']),
count($restrictedBy) > 2
? sprintf(
tp(
'Restricted by %s and %s as well as one other',
'Restricted by %s and %s as well as %d others',
count($restrictedBy) - 2
),
$restrictedBy[0]->getName(),
$restrictedBy[1]->getName(),
count($restrictedBy) - 2
)
: sprintf(
tp('Restricted by %s', 'Restricted by %s and %s', count($restrictedBy)),
...array_map(function ($role) {
return $role->getName();
}, $restrictedBy)
)
]);
} else {
$header->add([new Icon('filter'), t('Not restricted by any role')]);
}
$roles = [];
if (! empty($restrictions) && count($restrictions) > 1) {
list($combinedRestrictions, $combinedLinks) = $this->createRestrictionLinks($restriction, $restrictions);
$roles[] = new HtmlElement('li', null, [
new HtmlElement('div', ['class' => 'flex-overflow'], [
new HtmlElement('span', [
'class' => 'role',
'title' => t('All roles combined')
], join(' | ', array_map(function ($role) {
return $role->getName();
}, $restrictedBy))),
new HtmlElement('code', ['class' => 'restriction'], $combinedRestrictions)
]),
$combinedLinks ? new HtmlElement('div', ['class' => 'previews'], [
new HtmlElement('em', null, t('Previews:')),
$combinedLinks
]) : null
]);
}
foreach ($restrictedBy as $role) {
list($roleRestriction, $restrictionLinks) = $this->createRestrictionLinks(
$restriction,
[$role->getRestrictions($restriction)]
);
$roles[] = new HtmlElement('li', null, [
new HtmlElement('div', ['class' => 'flex-overflow'], [
new Link($role->getName(), Url::fromPath('role/edit', ['role' => $role->getName()]), [
'class' => 'role',
'title' => $role->getName()
]),
new HtmlElement('code', ['class' => 'restriction'], $roleRestriction)
]),
$restrictionLinks ? new HtmlElement('div', ['class' => 'previews'], [
new HtmlElement('em', null, t('Previews:')),
$restrictionLinks
]) : null
]);
}
return [
! empty($restrictedBy),
new HtmlElement('div', [
'class' => [empty($roles) ? null : 'collapsible', 'restrictions'],
'data-toggle-element' => '.collapsible-control',
'data-no-persistence' => true,
'data-visible-height' => 0
], [
empty($roles) ? $header : $header->addAttributes(['class' => 'collapsible-control']),
new HtmlElement('ul', null, $roles)
])
];
}
protected function assemble()
{
list($permissions, $restrictions) = RoleForm::collectProvidedPrivileges();
list($wildcardState, $wildcardAudit) = $this->auditPermission('*');
list($unrestrictedState, $unrestrictedAudit) = $this->auditPermission(self::UNRESTRICTED_PERMISSION);
$this->add(new HtmlElement('li', [
'class' => 'collapsible',
'data-toggle-element' => 'h3',
'data-visible-height' => 0
], [
new HtmlElement('h3', null, [
new HtmlElement('span', null, t('Administrative Privileges')),
new HtmlElement('span', ['class' => 'audit-preview'], [
$wildcardState || $unrestrictedState
? new Icon('check-circle', ['class' => 'granted'])
: null
])
]),
new HtmlElement('ol', ['class' => 'privilege-list'], [
new HtmlElement('li', null, [
new HtmlElement('p', ['class' => 'privilege-label'], t('Administrative Access')),
new HtmlElement('div', ['class' => 'spacer']),
$wildcardAudit
]),
new HtmlElement('li', null, [
new HtmlElement('p', ['class' => 'privilege-label'], t('Unrestricted Access')),
new HtmlElement('div', ['class' => 'spacer']),
$unrestrictedAudit
])
])
]));
$privilegeSources = array_unique(array_merge(array_keys($permissions), array_keys($restrictions)));
foreach ($privilegeSources as $source) {
$anythingGranted = false;
$anythingRefused = false;
$anythingRestricted = false;
$permissionList = new HtmlElement('ol', ['class' => 'privilege-list']);
foreach (isset($permissions[$source]) ? $permissions[$source] : [] as $permission => $metaData) {
list($permissionState, $permissionAudit) = $this->auditPermission($permission);
if ($permissionState !== null) {
if ($permissionState) {
$anythingGranted = true;
} else {
$anythingRefused = true;
}
}
$permissionList->add(new HtmlElement('li', null, [
new HtmlElement(
'p',
['class' => 'privilege-label'],
isset($metaData['label'])
? $metaData['label']
: array_map(function ($segment) {
return $segment[0] === '/' ? [
// Adds a zero-width char after each slash to help browsers break onto newlines
new HtmlString('/&#8203;'),
new HtmlElement('span', ['class' => 'no-wrap'], substr($segment, 1))
] : new HtmlElement('em', null, $segment);
}, preg_split(
'~(/[^/]+)~',
$permission,
-1,
PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
))
),
new HtmlElement('div', ['class' => 'spacer']),
$permissionAudit
]));
}
$restrictionList = new HtmlElement('ol', ['class' => 'privilege-list']);
foreach (isset($restrictions[$source]) ? $restrictions[$source] : [] as $restriction => $metaData) {
list($restrictionState, $restrictionAudit) = $this->auditRestriction($restriction);
if ($restrictionState) {
$anythingRestricted = true;
}
$restrictionList->add(new HtmlElement('li', null, [
new HtmlElement(
'p',
['class' => 'privilege-label'],
isset($metaData['label'])
? $metaData['label']
: array_map(function ($segment) {
return $segment[0] === '/' ? [
// Adds a zero-width char after each slash to help browsers break onto newlines
new HtmlString('/&#8203;'),
new HtmlElement('span', ['class' => 'no-wrap'], substr($segment, 1))
] : new HtmlElement('em', null, $segment);
}, preg_split(
'~(/[^/]+)~',
$restriction,
-1,
PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY
))
),
new HtmlElement('div', ['class' => 'spacer']),
$restrictionAudit
]));
}
if ($source === 'application') {
$label = 'Icinga Web 2';
} else {
$label = [$source, ' ', new HtmlElement('em', null, t('Module'))];
}
$this->add(new HtmlElement('li', [
'class' => 'collapsible',
'data-toggle-element' => 'h3',
'data-visible-height' => 0
], [
new HtmlElement('h3', null, [
new HtmlElement('span', null, $label),
new HtmlElement('span', ['class' => 'audit-preview'], [
$anythingGranted ? new Icon('check-circle', ['class' => 'granted']) : null,
$anythingRefused ? new Icon('times-circle', ['class' => 'refused']) : null,
$anythingRestricted ? new Icon('filter', ['class' => 'restricted']) : null
])
]),
$permissionList->isEmpty() ? null : [
new HtmlElement('h4', null, t('Permissions')),
$permissionList
],
$restrictionList->isEmpty() ? null : [
new HtmlElement('h4', null, t('Restrictions')),
$restrictionList
]
]));
}
}
private function collectRestrictions(Role $role, $restrictionName)
{
do {
$restriction = $role->getRestrictions($restrictionName);
if ($restriction) {
yield $role => $restriction;
}
} while (($role = $role->getParent()) !== null);
}
private function createRestrictionLinks($restrictionName, array $restrictions)
{
// TODO: Remove this hardcoded mess. Do this based on the restriction's meta data
switch ($restrictionName) {
case 'icingadb/filter/objects':
$filterString = join('|', $restrictions);
$list = new HtmlElement('ul', ['class' => 'links'], [
new HtmlElement('li', null, new Link(
'icingadb/hosts',
Url::fromPath('icingadb/hosts')->setQueryString($filterString)
)),
new HtmlElement('li', null, new Link(
'icingadb/services',
Url::fromPath('icingadb/services')->setQueryString($filterString)
)),
new HtmlElement('li', null, new Link(
'icingadb/hostgroups',
Url::fromPath('icingadb/hostgroups')->setQueryString($filterString)
)),
new HtmlElement('li', null, new Link(
'icingadb/servicegroups',
Url::fromPath('icingadb/servicegroups')->setQueryString($filterString)
))
]);
break;
case 'icingadb/filter/hosts':
$filterString = join('|', $restrictions);
$list = new HtmlElement('ul', ['class' => 'links'], [
new HtmlElement('li', null, new Link(
'icingadb/hosts',
Url::fromPath('icingadb/hosts')->setQueryString($filterString)
)),
new HtmlElement('li', null, new Link(
'icingadb/services',
Url::fromPath('icingadb/services')->setQueryString($filterString)
))
]);
break;
case 'icingadb/filter/services':
$filterString = join('|', $restrictions);
$list = new HtmlElement('ul', ['class' => 'links'], [
new HtmlElement('li', null, new Link(
'icingadb/services',
Url::fromPath('icingadb/services')->setQueryString($filterString)
))
]);
break;
case 'monitoring/filter/objects':
$filterString = join('|', $restrictions);
$list = new HtmlElement('ul', ['class' => 'links'], [
new HtmlElement('li', null, new Link(
'monitoring/list/hosts',
Url::fromPath('monitoring/list/hosts')->setQueryString($filterString)
)),
new HtmlElement('li', null, new Link(
'monitoring/list/services',
Url::fromPath('monitoring/list/services')->setQueryString($filterString)
)),
new HtmlElement('li', null, new Link(
'monitoring/list/hostgroups',
Url::fromPath('monitoring/list/hostgroups')->setQueryString($filterString)
)),
new HtmlElement('li', null, new Link(
'monitoring/list/servicegroups',
Url::fromPath('monitoring/list/servicegroups')->setQueryString($filterString)
))
]);
break;
case 'application/share/users':
$filter = Filter::any();
foreach ($restrictions as $roleRestriction) {
$userNames = StringHelper::trimSplit($roleRestriction);
foreach ($userNames as $userName) {
$filter->add(Filter::equal('user_name', $userName));
}
}
$filterString = QueryString::render($filter);
$list = new HtmlElement('ul', ['class' => 'links'], [
new HtmlElement('li', null, new Link(
'user/list',
Url::fromPath('user/list')->setQueryString($filterString)
))
]);
break;
case 'application/share/groups':
$filter = Filter::any();
foreach ($restrictions as $roleRestriction) {
$groupNames = StringHelper::trimSplit($roleRestriction);
foreach ($groupNames as $groupName) {
$filter->add(Filter::equal('group_name', $groupName));
}
}
$filterString = QueryString::render($filter);
$list = new HtmlElement('ul', ['class' => 'links'], [
new HtmlElement('li', null, new Link(
'group/list',
Url::fromPath('group/list')->setQueryString($filterString)
))
]);
break;
default:
$filterString = join(', ', $restrictions);
$list = null;
}
return [$filterString, $list];
}
}

View File

@ -0,0 +1,194 @@
<?php
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
namespace Icinga\Web\Widget;
use Icinga\Application\Icinga;
use ipl\Html\Form;
use ipl\Html\FormElement\InputElement;
use ipl\Html\HtmlElement;
use ipl\Web\Control\SearchBar\Suggestions;
use ipl\Web\Url;
class SingleValueSearchControl extends Form
{
/** @var string */
const DEFAULT_SEARCH_PARAMETER = 'q';
protected $defaultAttributes = ['class' => 'icinga-controls inline'];
/** @var string */
protected $searchParameter = self::DEFAULT_SEARCH_PARAMETER;
/** @var string */
protected $inputLabel;
/** @var string */
protected $submitLabel;
/** @var Url */
protected $suggestionUrl;
/** @var array */
protected $metaDataNames;
/**
* Set the search parameter to use
*
* @param string $name
* @return $this
*/
public function setSearchParameter($name)
{
$this->searchParameter = $name;
return $this;
}
/**
* Set the input's label
*
* @param string $label
*
* @return $this
*/
public function setInputLabel($label)
{
$this->inputLabel = $label;
return $this;
}
/**
* Set the submit button's label
*
* @param string $label
*
* @return $this
*/
public function setSubmitLabel($label)
{
$this->submitLabel = $label;
return $this;
}
/**
* Set the suggestion url
*
* @param Url $url
*
* @return $this
*/
public function setSuggestionUrl(Url $url)
{
$this->suggestionUrl = $url;
return $this;
}
/**
* Set names for which hidden meta data elements should be created
*
* @param string ...$names
*
* @return $this
*/
public function setMetaDataNames(...$names)
{
$this->metaDataNames = $names;
return $this;
}
protected function assemble()
{
$suggestionsId = Icinga::app()->getRequest()->protectId('single-value-suggestions');
$this->addElement(
'text',
$this->searchParameter,
[
'required' => true,
'minlength' => 1,
'autocomplete' => 'off',
'class' => 'search',
'data-enrichment-type' => 'completion',
'data-term-suggestions' => '#' . $suggestionsId,
'data-suggest-url' => $this->suggestionUrl,
'placeholder' => $this->inputLabel
]
);
if (! empty($this->metaDataNames)) {
$fieldset = new HtmlElement('fieldset');
foreach ($this->metaDataNames as $name) {
$hiddenElement = $this->createElement('hidden', $this->searchParameter . '-' . $name);
$this->registerElement($hiddenElement);
$fieldset->add($hiddenElement);
}
$this->getElement($this->searchParameter)->prependWrapper($fieldset);
}
$this->addElement(
'submit',
'btn_sumit',
[
'label' => $this->submitLabel,
'class' => 'btn-primary'
]
);
$this->add(new HtmlElement('div', [
'id' => $suggestionsId,
'class' => 'search-suggestions'
]));
}
/**
* Create a list of search suggestions based on the given groups
*
* @param array $groups
*
* @return HtmlElement
*/
public static function createSuggestions(array $groups)
{
$ul = new HtmlElement('ul');
foreach ($groups as list($name, $entries)) {
if ($name) {
if ($entries === false) {
$ul->add(new HtmlElement('li', ['class' => 'failure-message'], [
new HtmlElement('em', null, t('Can\'t search:')),
$name
]));
continue;
} elseif (empty($entries)) {
$ul->add(new HtmlElement('li', ['class' => 'failure-message'], [
new HtmlElement('em', null, t('No results:')),
$name
]));
continue;
} else {
$ul->add(new HtmlElement('li', ['class' => Suggestions::SUGGESTION_TITLE_CLASS], $name));
}
}
foreach ($entries as list($label, $metaData)) {
$attributes = [
'value' => $label,
'type' => 'button',
'tabindex' => -1
];
foreach ($metaData as $key => $value) {
$attributes['data-' . $key] = $value;
}
$ul->add(new HtmlElement('li', null, new InputElement(null, $attributes)));
}
}
return $ul;
}
}

View File

@ -0,0 +1,366 @@
// Style
.privilege-audit-role-control {
list-style-type: none;
li {
.rounded-corners(3px);
border: 1px solid @low-sat-blue;
&.active {
border-color: @icinga-blue;
}
}
}
.privilege-audit {
&, ul, ol {
list-style-type: none;
}
h3 {
border-bottom: 1px solid @gray-light;
}
h3 em,
.previews em,
.privilege-label em {
color: @text-color-light;
}
h3 em {
font-weight: normal;
}
.privilege-label em {
font-style: normal;
}
.icon {
color: @gray-light;
&.granted {
color: @color-granted;
}
&.refused {
color: @color-refused;
}
&.restricted {
color: @color-restricted;
}
}
.privilege-list > li {
.spacer {
opacity: 0;
.transition(opacity .5s ease-out);
}
&:hover .spacer {
.transition(opacity .25s .25s ease-in);
border: 0 dashed @gray-light;
border-top-width: .2em;
opacity: 1;
}
}
.vertical-line {
border: 0 solid;
border-left-width: 2px;
&.granted {
border-color: @color-granted;
}
&.refused {
border-color: @color-refused;
}
}
.connector {
border: 0 solid @gray-lighter;
border-bottom-width: 2px;
&.granted {
border-color: @color-granted;
}
&.refused {
border-color: @color-refused;
}
&:first-child {
border-width: 0 0 2px 2px;
border-bottom-left-radius: .5em;
}
}
.role {
.rounded-corners(1em);
border: 2px solid @gray-lighter;
&.granted {
border: 2px solid @color-granted;
}
&.refused {
border: 2px solid @color-refused;
}
}
.restriction {
font-family: @font-family-fixed;
background-color: @gray-lighter;
}
}
// Layout
.privilege-audit-role-control {
display: inline-flex;
flex-wrap: wrap;
margin: 0 0 0 1em;
padding: 0;
li {
margin-top: @vertical-padding;
&:not(:first-child) {
margin-left: .5em;
}
}
}
.privilege-audit {
&, ul, ol {
margin: 0;
padding: 0;
}
> li:not(.collapsed) {
margin-bottom: 2em;
}
.flex-overflow,
.privilege-list > li,
.inheritance-paths > ol {
display: flex;
}
.privilege-list > li {
margin-top: 1em;
> :last-child {
// This aids the usage of text-overflow:ellipsis in any of the children.
// It seems that to get this working while none of the children has a
// defined width, any flex item on the way up to the clipped container
// also must have a overflow value of "hidden".
// https://codepen.io/unthinkingly/pen/XMwJLG
overflow: hidden;
}
}
h3 {
display: flex;
> :first-child {
flex: 3 1 auto;
min-width: 20em;
max-width: 40em / 1.167em; // privilege label width + spacer width / h3 font-size
}
.audit-preview {
flex: 1 1 auto;
.icon:before {
width: 1.25em;
font-size: 1.25em / 1.167em; // privilege state icon font-size / h3 font-size
}
}
em {
font-size: .857em;
}
}
h4,
.privilege-label {
flex-shrink: 0;
width: 20em;
margin: 0;
text-align: right;
}
ol + h4 {
margin-top: 1em;
}
.spacer {
flex: 20 1 auto;
min-width: 10em; // TODO: Mobile?
max-width: 18.8em; // 20em - (margin-left + margin-right)
margin: .6em;
}
.inheritance-paths,
.restrictions {
flex: 1 1 auto;
> div {
line-height: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
> .icon:before {
width: 1.25em;
font-size: 1.25em;
}
}
}
.vertical-line {
margin-left: ~"calc(.75em - 1px)";
}
.connector {
flex: 1 1 auto;
width: 2em;
max-width: 2em;
min-width: 1em;
margin-bottom: ~"calc(1em - 1px)";
&:first-child {
margin-left: ~"calc(.75em - 1px)";
}
&.initiator {
z-index: 1;
margin-right: ~"calc(-.25em - 2px)";
}
}
.vertical-line + .connector {
min-width: ~"calc(.75em - 2px)";
width: ~"calc(.75em - 2px)";
flex-grow: 0;
&.initiator {
width: ~"calc(1em - 1px)";
}
}
.connector:first-child {
min-width: .75em;
width: .75em;
flex-grow: 0;
&.initiator {
width: 1em;
}
}
.role {
padding: .25em .5em .25em .5em;
line-height: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.icon:before {
font-size: 1.25em;
}
}
.inheritance-paths .role {
min-width: 4em;
margin-top: .5em;
padding-left: .25em;
}
.restrictions .role {
display: inline-block;
}
.previews {
display: flex;
margin-top: .25em;
em {
// explicit margin + ((header icon width + its margin right) * 125% font-size)
margin: 0 1em 0 1em + ((1.25em + .2em) * 1.25em);
}
}
.links li:not(:last-child):after {
content: ",";
}
.restrictions > ul > li {
margin-top: .5em;
.role {
margin-left: 1.25em + .2em * 1.25em; // (header icon width + its margin right) * 125% font-size
margin-right: 1em;
}
}
.restriction {
font-size: .8em;
padding: .335em / .8em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.user-select(all);
}
}
#layout.minimal-layout,
#layout.poor-layout {
.privilege-audit {
h3 > :first-child {
flex-grow: 99;
}
h4,
.privilege-label {
width: 12em;
}
.spacer {
flex: 0;
min-width: 0;
}
}
}
// Integrations
.privilege-audit .collapsible {
.collapsible-control {
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.collapsible-control:after {
content: "\f103";
display: inline-block;
font-family: 'ifont';
font-weight: normal;
padding: 0 .25em;
margin-right: .25em;
width: 1em;
opacity: .6;
float: right;
}
&.collapsed .collapsible-control:after {
content: "\e87a";
}
}

View File

@ -67,6 +67,11 @@
@menu-flyout-bg-color: @body-bg-color;
@menu-flyout-color: @text-color;
// Other colors
@color-granted: #59cd59;
@color-refused: #ee7373;
@color-restricted: #dede7d;
// Font families
@font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
@font-family-fixed: "Liberation Mono", "Lucida Console", Courier, monospace;

View File

@ -193,7 +193,6 @@ input.search {
input,
select {
padding: .5em;
max-width: 16em;
}

View File

@ -49,6 +49,12 @@ form.icinga-form {
form.inline {
display: inline-block;
fieldset {
display: inline-block;
vertical-align: top;
border: none;
}
}
// Minimal form layout
@ -300,6 +306,15 @@ form.icinga-form .form-controls {
}
form.inline {
:not([type="hidden"]) {
& ~ button:not([type]),
& ~ button[type="submit"],
& ~ input[type="submit"],
& ~ input[type="submit"].btn-confirm {
margin-left: @horizontal-padding;
}
}
button.noscript-apply {
margin-left: .5em;
padding: .1em;

View File

@ -45,6 +45,10 @@
}
}
.no-wrap {
white-space: nowrap;
}
.pull-right {
float: right;
}
@ -386,4 +390,4 @@ a:hover > .icon-cancel {
font-weight: bold;
}
}
}
}

View File

@ -15,7 +15,7 @@
cursor: pointer;
line-height: normal;
outline: none;
padding: @vertical-padding @horizontal-padding;
padding: ~"calc(@{vertical-padding} - 2px)" @horizontal-padding;
@duration: 0.2s;
// The trailing semicolon is needed to be able to pass this as a css list
@ -67,6 +67,13 @@
transform: @transform;
}
.user-select(@user-select) {
-webkit-user-select: @user-select;
-moz-user-select: @user-select;
-ms-user-select: @user-select;
user-select: @user-select;
}
.rounded-corners(@border-radius: 0.4em) {
border-radius: @border-radius;

View File

@ -255,11 +255,11 @@ form.role-form {
text-align: center;
&.icon-ok {
color: @color-ok;
color: @color-granted;
}
&.icon-cancel {
color: @color-critical;
color: @color-refused;
}
}
}