mirror of
https://github.com/Icinga/icingaweb2.git
synced 2025-07-24 22:34:24 +02:00
Merge pull request #4336 from Icinga/feature/audit-view-3053
Audit View
This commit is contained in:
commit
6f317ade30
@ -381,6 +381,16 @@ class GroupController extends AuthBackendController
|
|||||||
'url' => 'role/list'
|
'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')) {
|
if ($this->hasPermission('config/access-control/users')) {
|
||||||
|
@ -3,11 +3,24 @@
|
|||||||
|
|
||||||
namespace Icinga\Controllers;
|
namespace Icinga\Controllers;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use GuzzleHttp\Psr7\ServerRequest;
|
||||||
|
use Icinga\Authentication\AdmissionLoader;
|
||||||
|
use Icinga\Authentication\Auth;
|
||||||
use Icinga\Authentication\RolesConfig;
|
use Icinga\Authentication\RolesConfig;
|
||||||
|
use Icinga\Data\Selectable;
|
||||||
use Icinga\Exception\NotFoundError;
|
use Icinga\Exception\NotFoundError;
|
||||||
use Icinga\Forms\Security\RoleForm;
|
use Icinga\Forms\Security\RoleForm;
|
||||||
|
use Icinga\Repository\Repository;
|
||||||
use Icinga\Security\SecurityException;
|
use Icinga\Security\SecurityException;
|
||||||
|
use Icinga\User;
|
||||||
use Icinga\Web\Controller\AuthBackendController;
|
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
|
* Manage user permissions and restrictions based on roles
|
||||||
@ -71,7 +84,7 @@ class RoleController extends AuthBackendController
|
|||||||
$this->assertPermission('config/access-control/roles');
|
$this->assertPermission('config/access-control/roles');
|
||||||
|
|
||||||
$role = new RoleForm();
|
$role = new RoleForm();
|
||||||
$role->setRedirectUrl('role/list');
|
$role->setRedirectUrl('__CLOSE__');
|
||||||
$role->setRepository(new RolesConfig());
|
$role->setRepository(new RolesConfig());
|
||||||
$role->setSubmitLabel($this->translate('Create Role'));
|
$role->setSubmitLabel($this->translate('Create Role'));
|
||||||
$role->add()->handleRequest();
|
$role->add()->handleRequest();
|
||||||
@ -90,7 +103,7 @@ class RoleController extends AuthBackendController
|
|||||||
|
|
||||||
$name = $this->params->getRequired('role');
|
$name = $this->params->getRequired('role');
|
||||||
$role = new RoleForm();
|
$role = new RoleForm();
|
||||||
$role->setRedirectUrl('role/list');
|
$role->setRedirectUrl('__CLOSE__');
|
||||||
$role->setRepository(new RolesConfig());
|
$role->setRepository(new RolesConfig());
|
||||||
$role->setSubmitLabel($this->translate('Update Role'));
|
$role->setSubmitLabel($this->translate('Update Role'));
|
||||||
$role->edit($name);
|
$role->edit($name);
|
||||||
@ -113,7 +126,7 @@ class RoleController extends AuthBackendController
|
|||||||
|
|
||||||
$name = $this->params->getRequired('role');
|
$name = $this->params->getRequired('role');
|
||||||
$role = new RoleForm();
|
$role = new RoleForm();
|
||||||
$role->setRedirectUrl('role/list');
|
$role->setRedirectUrl('__CLOSE__');
|
||||||
$role->setRepository(new RolesConfig());
|
$role->setRepository(new RolesConfig());
|
||||||
$role->setSubmitLabel($this->translate('Remove Role'));
|
$role->setSubmitLabel($this->translate('Remove Role'));
|
||||||
$role->remove($name);
|
$role->remove($name);
|
||||||
@ -127,6 +140,204 @@ class RoleController extends AuthBackendController
|
|||||||
$this->renderForm($role, $this->translate('Remove Role'));
|
$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
|
* 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')) {
|
if ($this->hasPermission('config/access-control/users')) {
|
||||||
$tabs->add(
|
$tabs->add(
|
||||||
'user/list',
|
'user/list',
|
||||||
|
@ -338,6 +338,16 @@ class UserController extends AuthBackendController
|
|||||||
'url' => 'role/list'
|
'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(
|
$tabs->add(
|
||||||
|
@ -12,7 +12,6 @@ use Icinga\Forms\ConfigForm;
|
|||||||
use Icinga\Forms\RepositoryForm;
|
use Icinga\Forms\RepositoryForm;
|
||||||
use Icinga\Util\StringHelper;
|
use Icinga\Util\StringHelper;
|
||||||
use Icinga\Web\Notification;
|
use Icinga\Web\Notification;
|
||||||
use Zend_Form_Element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form for managing roles
|
* Form for managing roles
|
||||||
@ -47,133 +46,7 @@ class RoleForm extends RepositoryForm
|
|||||||
{
|
{
|
||||||
$this->setAttrib('class', self::DEFAULT_CLASSES . ' role-form');
|
$this->setAttrib('class', self::DEFAULT_CLASSES . ' role-form');
|
||||||
|
|
||||||
$helper = new Zend_Form_Element('bogus');
|
list($this->providedPermissions, $this->providedRestrictions) = static::collectProvidedPrivileges();
|
||||||
$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
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createFilter()
|
protected function createFilter()
|
||||||
@ -273,14 +146,14 @@ class RoleForm extends RepositoryForm
|
|||||||
|
|
||||||
$hasFullPerm = false;
|
$hasFullPerm = false;
|
||||||
foreach ($permissionList as $name => $spec) {
|
foreach ($permissionList as $name => $spec) {
|
||||||
$elementName = $name;
|
$elementName = $this->filterName($name);
|
||||||
if ($hasFullPerm || $hasAdminPerm) {
|
if ($hasFullPerm || $hasAdminPerm) {
|
||||||
$elementName .= '_fake';
|
$elementName .= '_fake';
|
||||||
}
|
}
|
||||||
|
|
||||||
$denyCheckbox = null;
|
$denyCheckbox = null;
|
||||||
if (! isset($spec['isFullPerm'])
|
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, [
|
$denyCheckbox = $this->createElement('checkbox', self::DENY_PREFIX . $name, [
|
||||||
'decorators' => ['ViewHelper']
|
'decorators' => ['ViewHelper']
|
||||||
@ -298,13 +171,25 @@ class RoleForm extends RepositoryForm
|
|||||||
'autosubmit' => isset($spec['isFullPerm']),
|
'autosubmit' => isset($spec['isFullPerm']),
|
||||||
'disabled' => $hasFullPerm || $hasAdminPerm ?: null,
|
'disabled' => $hasFullPerm || $hasAdminPerm ?: null,
|
||||||
'value' => $hasFullPerm || $hasAdminPerm,
|
'value' => $hasFullPerm || $hasAdminPerm,
|
||||||
'label' => preg_replace(
|
'label' => isset($spec['label'])
|
||||||
// Adds a zero-width char after each slash to help browsers break onto newlines
|
? $spec['label']
|
||||||
'~(?<!<)/~',
|
: join('', iterator_to_array(call_user_func(function ($segments) {
|
||||||
'/​',
|
foreach ($segments as $segment) {
|
||||||
isset($spec['label']) ? $spec['label'] : $spec['name']
|
if ($segment[0] === '/') {
|
||||||
),
|
// Adds a zero-width char after each slash to help browsers break onto newlines
|
||||||
'description' => isset($spec['description']) ? $spec['description'] : $spec['name'],
|
yield '/​';
|
||||||
|
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(
|
'decorators' => array_merge(
|
||||||
array_slice(self::$defaultElementDecorators, 0, 3),
|
array_slice(self::$defaultElementDecorators, 0, 3),
|
||||||
[['Callback', ['callback' => function () use ($denyCheckbox) {
|
[['Callback', ['callback' => function () use ($denyCheckbox) {
|
||||||
@ -320,11 +205,12 @@ class RoleForm extends RepositoryForm
|
|||||||
|
|
||||||
if ($hasFullPerm || $hasAdminPerm) {
|
if ($hasFullPerm || $hasAdminPerm) {
|
||||||
// Add a hidden element to preserve the configured permission value
|
// Add a hidden element to preserve the configured permission value
|
||||||
$this->addElement('hidden', $name);
|
$this->addElement('hidden', $this->filterName($name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($spec['isFullPerm'])) {
|
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) {
|
foreach ($this->providedRestrictions[$moduleName] as $name => $spec) {
|
||||||
$elements[] = $name;
|
$elementName = $this->filterName($name);
|
||||||
|
$elements[] = $elementName;
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'text',
|
'text',
|
||||||
$name,
|
$elementName,
|
||||||
[
|
[
|
||||||
'label' => preg_replace(
|
'label' => isset($spec['label'])
|
||||||
// Adds a zero-width char after each slash to help browsers break onto newlines
|
? $spec['label']
|
||||||
'~(?<!<)/~',
|
: join('', iterator_to_array(call_user_func(function ($segments) {
|
||||||
'/​',
|
foreach ($segments as $segment) {
|
||||||
isset($spec['label']) ? $spec['label'] : $spec['name']
|
if ($segment[0] === '/') {
|
||||||
),
|
// Add zero-width char after each slash to help browsers break onto newlines
|
||||||
|
yield '/​';
|
||||||
|
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'],
|
'description' => $spec['description'],
|
||||||
'style' => $isUnrestricted ? 'text-decoration:line-through;' : '',
|
'style' => $isUnrestricted ? 'text-decoration:line-through;' : '',
|
||||||
'readonly' => $isUnrestricted ?: null
|
'readonly' => $isUnrestricted ?: null
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
->getElement($name)
|
->getElement($elementName)
|
||||||
->getDecorator('Label')
|
->getDecorator('Label')
|
||||||
->setOption('escape', false);
|
->setOption('escape', false);
|
||||||
}
|
}
|
||||||
@ -402,11 +301,11 @@ class RoleForm extends RepositoryForm
|
|||||||
|
|
||||||
foreach ($this->providedPermissions as $moduleName => $permissionList) {
|
foreach ($this->providedPermissions as $moduleName => $permissionList) {
|
||||||
foreach ($permissionList as $name => $spec) {
|
foreach ($permissionList as $name => $spec) {
|
||||||
if (in_array($spec['name'], $permissions, true)) {
|
if (in_array($name, $permissions, true)) {
|
||||||
$values[$name] = 1;
|
$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;
|
$values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -415,8 +314,8 @@ class RoleForm extends RepositoryForm
|
|||||||
|
|
||||||
foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
|
foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
|
||||||
foreach ($restrictionList as $name => $spec) {
|
foreach ($restrictionList as $name => $spec) {
|
||||||
if (isset($role->{$spec['name']})) {
|
if (isset($role->$name)) {
|
||||||
$values[$name] = $role->{$spec['name']};
|
$values[$this->filterName($name)] = $role->$name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -430,9 +329,10 @@ class RoleForm extends RepositoryForm
|
|||||||
|
|
||||||
foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
|
foreach ($this->providedRestrictions as $moduleName => $restrictionList) {
|
||||||
foreach ($restrictionList as $name => $spec) {
|
foreach ($restrictionList as $name => $spec) {
|
||||||
if (isset($values[$name])) {
|
$elementName = $this->filterName($name);
|
||||||
$values[$spec['name']] = $values[$name];
|
if (isset($values[$elementName])) {
|
||||||
unset($values[$name]);
|
$values[$name] = $values[$elementName];
|
||||||
|
unset($values[$elementName]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -445,16 +345,17 @@ class RoleForm extends RepositoryForm
|
|||||||
$refusals = [];
|
$refusals = [];
|
||||||
foreach ($this->providedPermissions as $moduleName => $permissionList) {
|
foreach ($this->providedPermissions as $moduleName => $permissionList) {
|
||||||
foreach ($permissionList as $name => $spec) {
|
foreach ($permissionList as $name => $spec) {
|
||||||
if (isset($values[$name]) && $values[$name]) {
|
$elementName = $this->filterName($name);
|
||||||
$permissions[] = $spec['name'];
|
if (isset($values[$elementName]) && $values[$elementName]) {
|
||||||
|
$permissions[] = $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
$denyName = $this->filterName(self::DENY_PREFIX . $name);
|
$denyName = $this->filterName(self::DENY_PREFIX . $name);
|
||||||
if (isset($values[$denyName]) && $values[$denyName]) {
|
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)
|
protected function sortPermissions(&$permissions)
|
||||||
{
|
{
|
||||||
return uasort($permissions, function ($a, $b) {
|
return uksort($permissions, function ($a, $b) use ($permissions) {
|
||||||
if (isset($a['isUsagePerm'])) {
|
if (isset($permissions[$a]['isUsagePerm'])) {
|
||||||
return isset($b['isFullPerm']) ? 1 : -1;
|
return isset($permissions[$b]['isFullPerm']) ? 1 : -1;
|
||||||
} elseif (isset($b['isUsagePerm'])) {
|
} elseif (isset($permissions[$b]['isUsagePerm'])) {
|
||||||
return isset($a['isFullPerm']) ? -1 : 1;
|
return isset($permissions[$a]['isFullPerm']) ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$aParts = explode('/', $a['name']);
|
$aParts = explode('/', $a);
|
||||||
$bParts = explode('/', $b['name']);
|
$bParts = explode('/', $b);
|
||||||
|
|
||||||
do {
|
do {
|
||||||
$a = array_shift($aParts);
|
$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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,9 +179,15 @@ class AdmissionLoader
|
|||||||
$roles = [];
|
$roles = [];
|
||||||
$permissions = [];
|
$permissions = [];
|
||||||
$restrictions = [];
|
$restrictions = [];
|
||||||
|
$assignedRoles = [];
|
||||||
$isUnrestricted = false;
|
$isUnrestricted = false;
|
||||||
foreach ($this->roleConfig as $roleName => $roleConfig) {
|
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) {
|
foreach ($this->loadRole($roleName, $roleConfig) as $role) {
|
||||||
/** @var Role $role */
|
/** @var Role $role */
|
||||||
$roles[$role->getName()] = $role;
|
$roles[$role->getName()] = $role;
|
||||||
@ -206,6 +212,8 @@ class AdmissionLoader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user->setAdditional('assigned_roles', $assignedRoles);
|
||||||
|
|
||||||
$user->setIsUnrestricted($isUnrestricted);
|
$user->setIsUnrestricted($isUnrestricted);
|
||||||
$user->setRestrictions($isUnrestricted ? [] : $restrictions);
|
$user->setRestrictions($isUnrestricted ? [] : $restrictions);
|
||||||
$user->setPermissions($permissions);
|
$user->setPermissions($permissions);
|
||||||
|
@ -17,6 +17,7 @@ use Icinga\User;
|
|||||||
use Icinga\User\Preferences;
|
use Icinga\User\Preferences;
|
||||||
use Icinga\User\Preferences\PreferencesStore;
|
use Icinga\User\Preferences\PreferencesStore;
|
||||||
use Icinga\Web\Session;
|
use Icinga\Web\Session;
|
||||||
|
use Icinga\Web\StyleSheet;
|
||||||
|
|
||||||
class Auth
|
class Auth
|
||||||
{
|
{
|
||||||
@ -98,97 +99,23 @@ class Auth
|
|||||||
|
|
||||||
public function setAuthenticated(User $user, $persist = true)
|
public function setAuthenticated(User $user, $persist = true)
|
||||||
{
|
{
|
||||||
$username = $user->getUsername();
|
$this->setupUser($user);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Reload CSS if the theme changed
|
||||||
$groupBackend = UserGroupBackend::create($name, $config);
|
$themingConfig = Icinga::app()->getConfig()->getSection('themes');
|
||||||
$groupsFromBackend = $groupBackend->getMemberships($user);
|
$userTheme = $user->getPreferences()->getValue('icingaweb', 'theme');
|
||||||
} catch (Exception $e) {
|
if (! (bool) $themingConfig->get('disabled', false) && $userTheme !== null) {
|
||||||
Logger::error(
|
$defaultTheme = $themingConfig->get('default', StyleSheet::DEFAULT_THEME);
|
||||||
'Can\'t get group memberships for user \'%s\' from backend \'%s\'. An exception was thrown: %s',
|
if ($userTheme !== $defaultTheme) {
|
||||||
$username,
|
$this->getResponse()->setReloadCss(true);
|
||||||
$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);
|
|
||||||
$admissionLoader = new AdmissionLoader();
|
|
||||||
$admissionLoader->applyRoles($user);
|
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
if ($persist) {
|
if ($persist) {
|
||||||
$this->persistCurrentUser();
|
$this->persistCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
AuditHook::logActivity('login', 'User logged in');
|
AuditHook::logActivity('login', 'User logged in');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,4 +336,110 @@ class Auth
|
|||||||
$this->user = null;
|
$this->user = null;
|
||||||
Session::getSession()->purge();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,19 +253,20 @@ class Role
|
|||||||
*
|
*
|
||||||
* @param string $permission
|
* @param string $permission
|
||||||
* @param bool $ignoreParent Only evaluate the role's own permissions
|
* @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
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function grants($permission, $ignoreParent = false)
|
public function grants($permission, $ignoreParent = false, $cascadeUpwards = true)
|
||||||
{
|
{
|
||||||
foreach ($this->permissions as $grantedPermission) {
|
foreach ($this->permissions as $grantedPermission) {
|
||||||
if ($this->match($grantedPermission, $permission)) {
|
if ($this->match($grantedPermission, $permission, $cascadeUpwards)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $ignoreParent && $this->getParent() !== null) {
|
if (! $ignoreParent && $this->getParent() !== null) {
|
||||||
return $this->getParent()->grants($permission);
|
return $this->getParent()->grants($permission, false, $cascadeUpwards);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -3,19 +3,26 @@
|
|||||||
|
|
||||||
namespace Icinga\Web\Controller;
|
namespace Icinga\Web\Controller;
|
||||||
|
|
||||||
|
use ipl\Web\Compat\CompatController;
|
||||||
use Zend_Controller_Action_Exception;
|
use Zend_Controller_Action_Exception;
|
||||||
use Icinga\Application\Config;
|
use Icinga\Application\Config;
|
||||||
use Icinga\Authentication\User\UserBackend;
|
use Icinga\Authentication\User\UserBackend;
|
||||||
use Icinga\Authentication\User\UserBackendInterface;
|
use Icinga\Authentication\User\UserBackendInterface;
|
||||||
use Icinga\Authentication\UserGroup\UserGroupBackend;
|
use Icinga\Authentication\UserGroup\UserGroupBackend;
|
||||||
use Icinga\Authentication\UserGroup\UserGroupBackendInterface;
|
use Icinga\Authentication\UserGroup\UserGroupBackendInterface;
|
||||||
use Icinga\Web\Controller;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for authentication backend controllers
|
* 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
|
* Redirect to this controller's list action
|
||||||
*/
|
*/
|
||||||
|
@ -51,7 +51,8 @@ class StyleSheet
|
|||||||
'css/icinga/compat.less',
|
'css/icinga/compat.less',
|
||||||
'css/icinga/print.less',
|
'css/icinga/print.less',
|
||||||
'css/icinga/responsive.less',
|
'css/icinga/responsive.less',
|
||||||
'css/icinga/modal.less'
|
'css/icinga/modal.less',
|
||||||
|
'css/icinga/audit.less'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
545
library/Icinga/Web/View/PrivilegeAudit.php
Normal file
545
library/Icinga/Web/View/PrivilegeAudit.php
Normal 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('/​'),
|
||||||
|
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('/​'),
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
194
library/Icinga/Web/Widget/SingleValueSearchControl.php
Normal file
194
library/Icinga/Web/Widget/SingleValueSearchControl.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
366
public/css/icinga/audit.less
Normal file
366
public/css/icinga/audit.less
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
@ -67,6 +67,11 @@
|
|||||||
@menu-flyout-bg-color: @body-bg-color;
|
@menu-flyout-bg-color: @body-bg-color;
|
||||||
@menu-flyout-color: @text-color;
|
@menu-flyout-color: @text-color;
|
||||||
|
|
||||||
|
// Other colors
|
||||||
|
@color-granted: #59cd59;
|
||||||
|
@color-refused: #ee7373;
|
||||||
|
@color-restricted: #dede7d;
|
||||||
|
|
||||||
// Font families
|
// 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: -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;
|
@font-family-fixed: "Liberation Mono", "Lucida Console", Courier, monospace;
|
||||||
|
@ -193,7 +193,6 @@ input.search {
|
|||||||
|
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
padding: .5em;
|
|
||||||
max-width: 16em;
|
max-width: 16em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,12 @@ form.icinga-form {
|
|||||||
|
|
||||||
form.inline {
|
form.inline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimal form layout
|
// Minimal form layout
|
||||||
@ -300,6 +306,15 @@ form.icinga-form .form-controls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.inline {
|
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 {
|
button.noscript-apply {
|
||||||
margin-left: .5em;
|
margin-left: .5em;
|
||||||
padding: .1em;
|
padding: .1em;
|
||||||
|
@ -45,6 +45,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-wrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.pull-right {
|
.pull-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@ -386,4 +390,4 @@ a:hover > .icon-cancel {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: @vertical-padding @horizontal-padding;
|
padding: ~"calc(@{vertical-padding} - 2px)" @horizontal-padding;
|
||||||
|
|
||||||
@duration: 0.2s;
|
@duration: 0.2s;
|
||||||
// The trailing semicolon is needed to be able to pass this as a css list
|
// The trailing semicolon is needed to be able to pass this as a css list
|
||||||
@ -67,6 +67,13 @@
|
|||||||
transform: @transform;
|
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) {
|
.rounded-corners(@border-radius: 0.4em) {
|
||||||
border-radius: @border-radius;
|
border-radius: @border-radius;
|
||||||
|
|
||||||
|
@ -255,11 +255,11 @@ form.role-form {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&.icon-ok {
|
&.icon-ok {
|
||||||
color: @color-ok;
|
color: @color-granted;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.icon-cancel {
|
&.icon-cancel {
|
||||||
color: @color-critical;
|
color: @color-refused;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user