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'
|
||||
)
|
||||
);
|
||||
|
||||
$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')) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
'~(?<!<)/~',
|
||||
'/​',
|
||||
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 '/​';
|
||||
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
|
||||
'~(?<!<)/~',
|
||||
'/​',
|
||||
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 '/​';
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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'
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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-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;
|
||||
|
|
|
@ -193,7 +193,6 @@ input.search {
|
|||
|
||||
input,
|
||||
select {
|
||||
padding: .5em;
|
||||
max-width: 16em;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -45,6 +45,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
float: right;
|
||||
}
|
||||
|
@ -386,4 +390,4 @@ a:hover > .icon-cancel {
|
|||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue