Merge b786e0c314d7c265f946b9aac20588fba502db8b into 2864e60d7879a648e915d870c6a7bb3153ea5c19

This commit is contained in:
Alexander Aleksandrovič Klimov 2024-11-01 14:28:47 +01:00 committed by GitHub
commit 4c41265d4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1391 additions and 68 deletions

@ -3,8 +3,13 @@
namespace Icinga\Controllers;
use DateTime;
use Exception;
use Icinga\Application\Version;
use Icinga\Common\Database;
use Icinga\Forms\Security\RoleForm;
use Icinga\Model\Role;
use Icinga\Util\StringHelper;
use InvalidArgumentException;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
@ -23,12 +28,18 @@ use Icinga\Web\Controller;
use Icinga\Web\Notification;
use Icinga\Web\Url;
use Icinga\Web\Widget;
use ipl\Sql\Connection;
use ipl\Sql\Insert;
use ipl\Sql\Select;
use ipl\Sql\Update;
/**
* Application and module configuration
*/
class ConfigController extends Controller
{
use Database;
/**
* Create and return the tabs to display when showing application configuration
*/
@ -99,13 +110,21 @@ class ConfigController extends Controller
$form->setOnSuccess(function (GeneralConfigForm $form) {
$config = Config::app();
$useStrictCsp = (bool) $config->get('security', 'use_strict_csp', false);
$storeRolesInDb = (bool) $config->get('global', 'store_roles_in_db', false);
if ($form->onSuccess() === false) {
return false;
}
$appConfigForm = $form->getSubForm('form_config_general_application');
if ($appConfigForm && (bool) $appConfigForm->getValue('security_use_strict_csp') !== $useStrictCsp) {
$this->getResponse()->setReloadWindow(true);
if ($appConfigForm) {
if ((bool) $appConfigForm->getValue('security_use_strict_csp') !== $useStrictCsp) {
$this->getResponse()->setReloadWindow(true);
}
if (! $storeRolesInDb && $appConfigForm->getValue('global_store_roles_in_db')) {
$this->migrateRolesToFreshDb();
}
}
})->handleRequest();
@ -114,6 +133,106 @@ class ConfigController extends Controller
$this->createApplicationTabs()->activate('general');
}
/**
* Migrate roles.ini to database if the latter contains no roles
*/
private function migrateRolesToFreshDb(): void
{
$roles = Config::app('roles');
$now = (new DateTime())->getTimestamp() * 1000;
$this->getDb()->transaction(function (Connection $db) use ($roles, $now) {
if (Role::on($db)->columns('id')->first()) {
return;
}
foreach ($roles as $name => $role) {
$db->prepexec(
(new Insert())
->into('icingaweb_role')
->columns(['name', 'unrestricted', 'ctime'])
->values([$name, $role->unrestricted ? 'y' : 'n', $now])
);
$id = $db->lastInsertId();
$permissions = StringHelper::trimSplit($role->permissions);
$refusals = StringHelper::trimSplit($role->refusals);
$permissionsAndRefusals = [];
foreach (StringHelper::trimSplit($role->users) as $user) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_user')
->columns(['role_id', 'user_name'])
->values([$id, $user])
);
}
foreach (StringHelper::trimSplit($role->groups) as $group) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_group')
->columns(['role_id', 'group_name'])
->values([$id, $group])
);
}
foreach ([$permissions, $refusals] as $permissionsOrRefusals) {
foreach ($permissionsOrRefusals as $permissionOrRefusal) {
$permissionsAndRefusals[$permissionOrRefusal] = ['allowed' => 'n', 'denied' => 'n'];
}
}
foreach ($permissions as $permission) {
$permissionsAndRefusals[$permission]['allowed'] = 'y';
}
foreach ($refusals as $refusal) {
$permissionsAndRefusals[$refusal]['denied'] = 'y';
}
foreach ($permissionsAndRefusals as $permission => $authz) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_permission')
->columns(['role_id', 'permission', 'allowed', 'denied'])
->values([$id, $permission, $authz['allowed'], $authz['denied']])
);
}
foreach (RoleForm::collectProvidedPrivileges()[1] as $restrictionList) {
foreach ($restrictionList as $restriction => $_) {
if (isset($role->$restriction)) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_restriction')
->columns(['role_id', 'restriction', 'filter'])
->values([$id, $restriction, $role->$restriction])
);
}
}
}
}
foreach ($roles as $name => $role) {
if (isset($role->parent)) {
$db->prepexec(
(new Update())
->table('icingaweb_role')
->set([
'parent_id' => (new Select())
->from('icingaweb_role')
->where(['name = ?' => $role->parent])
->columns(['id'])
,
])
->where(['name = ?' => $name])
);
}
}
});
}
/**
* Display the list of all modules
*/

@ -5,21 +5,28 @@ namespace Icinga\Controllers;
use Exception;
use GuzzleHttp\Psr7\ServerRequest;
use Icinga\Application\Config;
use Icinga\Authentication\AdmissionLoader;
use Icinga\Authentication\Auth;
use Icinga\Authentication\RolesConfig;
use Icinga\Authentication\User\DomainAwareInterface;
use Icinga\Common\Database;
use Icinga\Data\Selectable;
use Icinga\Exception\NotFoundError;
use Icinga\Forms\Security\RoleDbForm;
use Icinga\Forms\Security\RoleForm;
use Icinga\Model\Role;
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\RolesTable;
use Icinga\Web\Widget\SingleValueSearchControl;
use ipl\Html\Html;
use ipl\Html\HtmlString;
use ipl\Web\Compat\SearchControls;
use ipl\Web\Filter\QueryString;
use ipl\Web\Url;
use ipl\Web\Widget\Link;
@ -30,6 +37,9 @@ use ipl\Web\Widget\Link;
*/
class RoleController extends AuthBackendController
{
use Database;
use SearchControls;
public function init()
{
$this->assertPermission('config/access-control/roles');
@ -59,20 +69,70 @@ class RoleController extends AuthBackendController
public function listAction()
{
$this->createListTabs()->activate('role/list');
$this->view->roles = (new RolesConfig())
->select();
$sortAndFilterColumns = [
'name' => $this->translate('Name'),
'users' => $this->translate('Users'),
'groups' => $this->translate('Groups'),
'permissions' => $this->translate('Permissions')
];
if (Config::app()->get('global', 'store_roles_in_db')) {
$db = $this->getDb();
$query = Role::on($db)->with('parent');
$this->setupFilterControl($this->view->roles, $sortAndFilterColumns, ['name']);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->roles);
$this->setupSortControl($sortAndFilterColumns, $this->view->roles, ['name']);
$limitControl = $this->createLimitControl();
$sortControl = $this->createSortControl($query, ['name' => $this->translate('Name')]);
$paginationControl = $this->createPaginationControl($query);
$searchBar = $this->createSearchBar($query, [$limitControl->getLimitParam(), $sortControl->getSortParam()]);
if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) {
if ($searchBar->hasBeenSubmitted()) {
$filter = QueryString::parse((string) $this->params);
} else {
$this->addControl($searchBar);
$this->sendMultipartUpdate();
return;
}
} else {
$filter = $searchBar->getFilter();
}
$query->filter($filter);
$this->addControl($paginationControl);
$this->addControl($limitControl);
$this->addControl($sortControl);
$this->addControl($searchBar);
$this->addControl(Html::tag(
'a',
[
'href' => Url::fromPath('role/add'),
'data-base-target' => '_next',
'class' => 'button-link icon-plus'
],
$this->translate('Create a New Role')
));
if ($query->count()) {
$this->addContent((new RolesTable())->setRoles($query));
} else {
$this->addContent(Html::tag('p', $this->translate('No roles found.')));
}
if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) {
$this->sendMultipartUpdate();
}
} else {
$this->view->roles = (new RolesConfig())->select();
$sortAndFilterColumns = [
'name' => $this->translate('Name'),
'users' => $this->translate('Users'),
'groups' => $this->translate('Groups'),
'permissions' => $this->translate('Permissions')
];
$this->setupFilterControl($this->view->roles, $sortAndFilterColumns, ['name']);
$this->setupLimitControl();
$this->setupPaginationControl($this->view->roles);
$this->setupSortControl($sortAndFilterColumns, $this->view->roles, ['name']);
}
}
/**
@ -82,9 +142,8 @@ class RoleController extends AuthBackendController
*/
public function addAction()
{
$role = new RoleForm();
$role = $this->prepareForm();
$role->setRedirectUrl('__CLOSE__');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Create Role'));
$role->add()->handleRequest();
@ -99,9 +158,8 @@ class RoleController extends AuthBackendController
public function editAction()
{
$name = $this->params->getRequired('role');
$role = new RoleForm();
$role = $this->prepareForm();
$role->setRedirectUrl('__CLOSE__');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Update Role'));
$role->edit($name);
@ -120,9 +178,8 @@ class RoleController extends AuthBackendController
public function removeAction()
{
$name = $this->params->getRequired('role');
$role = new RoleForm();
$role = $this->prepareForm();
$role->setRedirectUrl('__CLOSE__');
$role->setRepository(new RolesConfig());
$role->setSubmitLabel($this->translate('Remove Role'));
$role->remove($name);
@ -389,4 +446,16 @@ class RoleController extends AuthBackendController
return $tabs;
}
/**
* Create a form for role addition/modification/deletion and set the storage
*
* @return RoleForm
*/
private function prepareForm(): RoleForm
{
return Config::app()->get('global', 'store_roles_in_db')
? (new RoleDbForm())->setDb($this->getDb())
: (new RoleForm())->setRepository(new RolesConfig());
}
}

@ -100,6 +100,17 @@ class ApplicationConfigForm extends Form
)
);
$this->addElement(
'checkbox',
'global_store_roles_in_db',
[
'label' => $this->translate('Store Roles in Database'),
'description' => $this->translate(
'Set whether to store roles used for access control in the database selected above.'
)
]
);
return $this;
}
}

@ -0,0 +1,330 @@
<?php
/* Icinga Web 2 | (c) 2024 Icinga GmbH | GPLv2+ */
namespace Icinga\Forms\Security;
use DateTime;
use Icinga\Application\Modules\Manager;
use Icinga\Model\Role;
use Icinga\Util\StringHelper;
use ipl\Sql\Connection;
use ipl\Sql\Delete;
use ipl\Sql\Insert;
use ipl\Sql\Select;
use ipl\Sql\Update;
use ipl\Stdlib\Filter;
/**
* Form for managing roles stored in the database
*/
class RoleDbForm extends RoleForm
{
/**
* Database where the roles are stored
*
* @var ?Connection
*/
private $db = null;
public function fetchEntry()
{
$role = Role::on($this->db)->with('parent')->filter(Filter::equal('name', $this->getIdentifier()))->first();
if (! $role) {
return false;
}
$values = [
'name' => $role->name,
'unrestricted' => (int) $role->unrestricted
];
$users = [];
$groups = [];
$permissions = [];
$refusals = [];
$restrictions = [];
foreach ($role->users as $user) {
$users[] = $user->user_name;
}
foreach ($role->groups as $group) {
$groups[] = $group->group_name;
}
foreach ($role->permissions as $permission) {
if ($permission->allowed) {
$permissions[$permission->permission] = true;
if ($permission->permission === '*') {
$values[self::WILDCARD_NAME] = 1;
}
}
if ($permission->denied) {
$refusals[$permission->permission] = true;
}
}
foreach ($role->restrictions as $restriction) {
$restrictions[$restriction->restriction] = $restriction->filter;
}
if ($role->parent) {
$values['parent'] = $role->parent->name;
}
if ($users) {
sort($users);
$values['users'] = implode(',', $users);
}
if ($groups) {
sort($groups);
$values['groups'] = implode(',', $groups);
}
if ($permissions || $refusals) {
foreach ($this->providedPermissions as $moduleName => $permissionList) {
$hasFullPerm = false;
foreach ($permissionList as $name => $spec) {
if (array_key_exists($name, $permissions)) {
$values[$this->filterName($name)] = 1;
if (isset($spec['isFullPerm'])) {
$hasFullPerm = true;
}
}
if (array_key_exists($name, $refusals)) {
$values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
}
}
if ($hasFullPerm) {
unset($values[$this->filterName(Manager::MODULE_PERMISSION_NS . $moduleName)]);
}
}
}
if ($restrictions) {
foreach ($this->providedRestrictions as $restrictionList) {
foreach ($restrictionList as $name => $spec) {
if (array_key_exists($name, $restrictions)) {
$values[$this->filterName($name)] = $restrictions[$name];
}
}
}
}
return (object) $values;
}
protected function entryExists(): bool
{
return Role::on($this->db)->filter(Filter::equal('name', $this->getIdentifier()))->count() > 0;
}
protected function insertEntry(): void
{
$values = $this->getValues();
$this->db->transaction(function (Connection $db) use ($values) {
$db->prepexec(
(new Insert())
->into('icingaweb_role')
->columns(['parent_id', 'name', 'unrestricted', 'ctime'])
->values([
$this->queryRoleId($db, $values['parent']),
$values['name'],
$values['unrestricted'] ? 'y' : 'n',
(new DateTime())->getTimestamp() * 1000
])
);
$this->insertChildTables($db, $db->lastInsertId(), $values);
});
}
protected function updateEntry(): void
{
$values = $this->getValues();
$this->db->transaction(function (Connection $db) use ($values) {
$id = $this->queryRoleId($db, $this->getIdentifier());
$db->prepexec(
(new Update())
->table('icingaweb_role')
->set([
'parent_id' => $this->queryRoleId($db, $values['parent']),
'name' => $values['name'],
'unrestricted' => $values['unrestricted'] ? 'y' : 'n',
'mtime' => (new DateTime())->getTimestamp() * 1000
])
->where(['id = ?' => $id])
);
$db->prepexec((new Delete())->from('icingaweb_role_user')->where(['role_id = ?' => $id]));
$db->prepexec((new Delete())->from('icingaweb_role_group')->where(['role_id = ?' => $id]));
$db->prepexec((new Delete())->from('icingaweb_role_permission')->where(['role_id = ?' => $id]));
$db->prepexec((new Delete())->from('icingaweb_role_restriction')->where(['role_id = ?' => $id]));
$this->insertChildTables($db, $id, $values);
});
}
/**
* Query the ID of a role
*
* @param Connection $db Database to operate on
* @param ?string $name Target role name
*
* @return ?int Target role ID or null
*/
private function queryRoleId(Connection $db, ?string $name): ?int
{
if ($name !== null) {
$role = Role::on($db)->filter(Filter::equal('name', $name))->columns('id')->first();
if ($role) {
return $role->id;
}
}
return null;
}
/**
* Populate icingaweb_role_* tables for a new role
*
* @param Connection $db Database to operate on
* @param int $id Role ID
* @param array $values Role data as from {@link getValues()}
*/
private function insertChildTables(Connection $db, int $id, array $values): void
{
$permissions = StringHelper::trimSplit($values['permissions']);
$refusals = StringHelper::trimSplit($values['refusals']);
$permissionsAndRefusals = [];
foreach (StringHelper::trimSplit($values['users']) as $user) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_user')
->columns(['role_id', 'user_name'])
->values([$id, $user])
);
}
foreach (StringHelper::trimSplit($values['groups']) as $group) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_group')
->columns(['role_id', 'group_name'])
->values([$id, $group])
);
}
foreach ([$permissions, $refusals] as $permissionsOrRefusals) {
foreach ($permissionsOrRefusals as $permissionOrRefusal) {
$permissionsAndRefusals[$permissionOrRefusal] = ['allowed' => 'n', 'denied' => 'n'];
}
}
foreach ($permissions as $permission) {
$permissionsAndRefusals[$permission]['allowed'] = 'y';
}
foreach ($refusals as $refusal) {
$permissionsAndRefusals[$refusal]['denied'] = 'y';
}
foreach ($permissionsAndRefusals as $name => $authz) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_permission')
->columns(['role_id', 'permission', 'allowed', 'denied'])
->values([$id, $name, $authz['allowed'], $authz['denied']])
);
}
foreach ($this->providedRestrictions as $restrictionList) {
foreach ($restrictionList as $name => $_) {
if (isset($values[$name])) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_restriction')
->columns(['role_id', 'restriction', 'filter'])
->values([$id, $name, $values[$name]])
);
}
}
}
}
protected function deleteEntry(): void
{
$this->db->prepexec((new Delete())->from('icingaweb_role')->where(['name = ?' => $this->getIdentifier()]));
}
protected function collectRoles(): array
{
$roles = [];
$name = $this->getIdentifier();
if ($name === null) {
foreach (Role::on($this->db)->orderBy('name')->columns('name') as $role) {
$roles[$role->name] = $role->name;
}
} else {
$query = (new Select())
->with(
(new Select())
->from('icingaweb_role')
->where(['name = ?' => $name])
->columns(['id', 'parent_id'])
->union( // handle circular relationships
(new Select())
->from(['r' => 'icingaweb_role'])
->join('rl', 'rl.id = r.parent_id')
->columns(['r.id', 'r.parent_id'])
),
'rl',
true
)
->from('icingaweb_role')
->where(['id NOT IN ?' => (new Select())->from('rl')->columns('id')])
->orderBy('name')
->columns('name');
foreach ($this->db->select($query) as $row) {
$roles[$row->name] = $row->name;
}
}
return $roles;
}
protected function onRenameSuccess(string $oldName, ?string $newName): void
{
// Already handled by the database schema
}
/**
* Set the database where the roles are stored
*
* @param Connection $db
*
* @return $this
*/
public function setDb(Connection $db): self
{
$this->db = $db;
return $this;
}
}

@ -511,11 +511,7 @@ class RoleForm extends RepositoryForm
}
if ($this->getIdentifier() && ($newName = $this->getValue('name')) !== $this->getIdentifier()) {
$this->repository->update(
$this->getBaseTable(),
['parent' => $newName],
Filter::where('parent', $this->getIdentifier())
);
$this->onRenameSuccess($this->getIdentifier(), $newName);
}
if (ConfigFormEventsHook::runOnSuccess($this) === false) {
@ -526,6 +522,17 @@ class RoleForm extends RepositoryForm
}
}
/**
* Update child roles of role $oldName, set their parent to $newName
*
* @param string $oldName
* @param string $newName
*/
protected function onRenameSuccess(string $oldName, ?string $newName): void
{
$this->repository->update($this->getBaseTable(), ['parent' => $newName], Filter::where('parent', $oldName));
}
/**
* Collect permissions and restrictions provided by Icinga Web 2 and modules
*

@ -29,14 +29,16 @@ Option | Description
show\_stacktraces | **Optional.** Whether to show debug stacktraces. Defaults to `0`.
module\_path | **Optional.** Specifies the directories where modules can be installed. Multiple directories must be separated with colons.
config\_resource | **Required.** Specify a defined [resource](04-Resources.md#resources-configuration-database) name.
store\_roles\_in\_db | **Optional.** Whether to store roles used for access control in the database specified above. Defaults to `0`.
Example for storing the user preferences in the database resource `icingaweb_db`:
Example for storing the user preferences and roles in the database resource `icingaweb_db`:
```
[global]
show_stacktraces = "0"
config_resource = "icingaweb_db"
store_roles_in_db = "1"
module_path = "/usr/share/icingaweb2/modules"
```

@ -3,20 +3,29 @@
namespace Icinga\Authentication;
use Exception;
use Generator;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Common\Database;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError;
use Icinga\Data\ConfigObject;
use Icinga\Model\Role as RoleModel;
use Icinga\Model\RolePermission;
use Icinga\Model\RoleRestriction;
use Icinga\User;
use Icinga\Util\StringHelper;
use ipl\Sql\Connection;
use ipl\Sql\Select;
use ipl\Stdlib\Filter;
/**
* Retrieve restrictions and permissions for users
*/
class AdmissionLoader
{
use Database;
const LEGACY_PERMISSIONS = [
'admin' => 'application/announcements',
'application/stacktraces' => 'user/application/stacktraces',
@ -52,12 +61,27 @@ class AdmissionLoader
/** @var ConfigObject */
protected $roleConfig;
/**
* Database where the roles are stored
*
* @var ?Connection
*/
protected $rolesDb = null;
public function __construct()
{
try {
$this->roleConfig = Config::app('roles');
} catch (NotReadableError $e) {
Logger::error('Can\'t access roles configuration. An exception was thrown:', $e);
if (Config::app()->get('global', 'store_roles_in_db')) {
$db = $this->getDb();
RoleModel::on($db)->limit(1)->columns('id')->first();
$this->rolesDb = $db;
} else {
$this->roleConfig = Config::app('roles');
}
} catch (Exception $e) {
Logger::error('Can\'t access roles storage. An exception was thrown:', $e);
}
}
@ -170,6 +194,10 @@ class AdmissionLoader
*/
public function applyRoles(User $user)
{
if ($this->rolesDb !== null) {
$this->applyDbRoles($user);
}
if ($this->roleConfig === null) {
return;
}
@ -229,6 +257,138 @@ class AdmissionLoader
$user->setRoles(array_values($roles));
}
/**
* Apply permissions, restrictions and roles from the database to the given user
*
* @param User $user
*/
private function applyDbRoles(User $user): void
{
$direct = (new Select())
->from('icingaweb_role')
->where([
'id IN ?' => (new Select())
->from('icingaweb_role_user')
->where(['user_name IN (?)' => [$user->getUsername(), '*']])
->columns('role_id')
])
->columns(['id', 'parent_id', 'name', 'unrestricted', 'direct' => '1']);
$userGroups = $user->getGroups();
$roleData = [];
$roles = [];
$assignedRoles = [];
$unrestricted = false;
if ($userGroups) {
$userGroups = array_values($userGroups);
$direct->orWhere([
'id IN ?' => (new Select())
->from('icingaweb_role_group')
->where(['group_name IN (?)' => $userGroups])
->columns('role_id')
]);
}
// Not a UNION ALL to handle circular relationships.
// Due to the "direct" column such may still appear twice.
// Hence ORDER BY direct, so that the last one (direct=1) wins.
$query = (new Select())
->with(
$direct->union(
(new Select())
->from(['r' => 'icingaweb_role'])
->join('rl', 'rl.parent_id = r.id')
->columns(['r.id', 'r.parent_id', 'r.name', 'r.unrestricted', 'direct' => '0'])
),
'rl',
true
)
->from('rl')
->orderBy('direct')
->columns(['id', 'parent_id', 'name', 'unrestricted', 'direct']);
foreach ($this->rolesDb->select($query) as $row) {
$roleData[$row->id] = $row;
}
foreach ($roleData as $row) {
$roles[$row->id] = (new Role())
->setName($row->name)
->setIsUnrestricted($row->unrestricted);
if ($row->direct) {
$assignedRoles[] = $row->name;
}
if ($row->unrestricted) {
$unrestricted = true;
}
}
foreach ($roleData as $row) {
if ($row->parent_id) {
$parent = $roles[$row->parent_id];
$child = $roles[$row->id];
$child->setParent($parent);
$parent->addChild($child);
}
}
$filter = Filter::equal('role_id', array_keys($roles));
$permissions = [];
$allPermissions = [];
$refusals = [];
$restrictions = [];
$allRestrictions = [];
foreach (RolePermission::on($this->rolesDb)->filter($filter) as $row) {
if ($row->allowed) {
$permissions[$row->role_id][] = $row->permission;
}
if ($row->denied) {
$refusals[$row->role_id][] = $row->permission;
}
}
foreach ($permissions as $roleId => & $rolePermissions) {
list($rolePermissions, $newRefusals) = $this->migrateLegacyPermissions($rolePermissions);
if ($newRefusals) {
array_push($refusals[$roleId], ...$newRefusals);
}
$roles[$roleId]->setPermissions($rolePermissions);
array_push($allPermissions, ...$rolePermissions);
}
foreach ($refusals as $roleId => $roleRefusals) {
$roles[$roleId]->setRefusals($roleRefusals);
}
foreach (RoleRestriction::on($this->rolesDb)->filter($filter) as $row) {
$restrictions[$row->role_id][$row->restriction] = $row->filter;
}
foreach ($restrictions as $roleId => & $roleRestrictions) {
foreach ($roleRestrictions as $name => & $restriction) {
$restriction = str_replace('$user.local_name$', $user->getLocalUsername(), $restriction);
$allRestrictions[$name][] = $restriction;
}
$roles[$roleId]->setRestrictions($roleRestrictions);
}
$user->setAdditional('assigned_roles', $assignedRoles);
$user->setIsUnrestricted($unrestricted);
$user->setRestrictions($unrestricted ? [] : $allRestrictions);
$user->setPermissions(array_values(array_unique($allPermissions)));
$user->setRoles(array_values($roles));
}
public static function migrateLegacyPermissions(array $permissions)
{
$migratedGrants = [];

@ -4,6 +4,7 @@
namespace Icinga\Common;
use Icinga\Application\Config as IcingaConfig;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use ipl\Sql\Config as SqlConfig;
use ipl\Sql\Connection;
@ -18,17 +19,19 @@ trait Database
/**
* Get a connection to the Icinga Web database
*
* @param ?ConfigObject $params Optional custom database specification
*
* @return Connection
*
* @throws \Icinga\Exception\ConfigurationError
*/
protected function getDb(): Connection
protected function getDb(ConfigObject $params = null): Connection
{
if (! $this->hasDb()) {
if ($params === null && ! $this->hasDb()) {
throw new LogicException('Please check if a db instance exists at all');
}
$config = new SqlConfig(ResourceFactory::getResourceConfig(
$config = new SqlConfig($params ?? ResourceFactory::getResourceConfig(
IcingaConfig::app()->get('global', 'config_resource')
));
if ($config->db === 'mysql') {

@ -0,0 +1,79 @@
<?php
/* Icinga Web 2 | (c) 2024 Icinga GmbH | GPLv2+ */
namespace Icinga\Model;
use DateTime;
use ipl\Orm\Behavior\BoolCast;
use ipl\Orm\Behavior\MillisecondTimestamp;
use ipl\Orm\Behaviors;
use ipl\Orm\Model;
use ipl\Orm\Relations;
/**
* A database model for Icinga Web role table
*
* @property int $id Unique identifier
* @property ?int $parent_id Inherited role identifier (optional)
* @property string $name Unique name
* @property bool $unrestricted Whether restrictions don't apply
* @property DateTime $ctime The insert time
* @property ?DateTime $mtime The modification time (optional)
* @property ?Role $parent Inherited role (optional)
* @property Role[] $children Inheriting roles
* @property RoleUser[] $users Users this role applies to
* @property RoleGroup[] $groups Groups this role applies to
* @property RolePermission[] $permissions Permissions this role allows/denies
* @property RoleRestriction[] $restrictions Restrictions this role imposes
*/
class Role extends Model
{
public function getTableName(): string
{
return 'icingaweb_role';
}
public function getKeyName(): string
{
return 'id';
}
public function getColumns(): array
{
return ['parent_id', 'name', 'unrestricted', 'ctime', 'mtime'];
}
public function createBehaviors(Behaviors $behaviors): void
{
$behaviors->add(new BoolCast(['unrestricted']));
$behaviors->add(new MillisecondTimestamp(['ctime', 'mtime']));
}
public function createRelations(Relations $relations): void
{
$relations->belongsTo('parent', self::class)
->setCandidateKey('parent_id')
->setJoinType('LEFT');
$relations->hasMany('children', self::class)
->setForeignKey('parent_id')
->setJoinType('LEFT');
$relations->hasMany('users', RoleUser::class)
->setForeignKey('role_id')
->setJoinType('LEFT');
$relations->hasMany('groups', RoleGroup::class)
->setForeignKey('role_id')
->setJoinType('LEFT');
$relations->hasMany('permissions', RolePermission::class)
->setForeignKey('role_id')
->setJoinType('LEFT');
$relations->hasMany('restrictions', RoleRestriction::class)
->setForeignKey('role_id')
->setJoinType('LEFT');
}
}

@ -0,0 +1,40 @@
<?php
/* Icinga Web 2 | (c) 2024 Icinga GmbH | GPLv2+ */
namespace Icinga\Model;
use ipl\Orm\Model;
use ipl\Orm\Relations;
/**
* A database model for Icinga Web role-group table
*
* @property int $role_id Role identifier
* @property string $group_name Group name
* @property Role $role Role object
*/
class RoleGroup extends Model
{
public function getTableName(): string
{
return 'icingaweb_role_group';
}
public function getKeyName(): array
{
return ['group_name', 'role_id'];
}
public function getColumns(): array
{
return [];
}
public function createRelations(Relations $relations): void
{
$relations->belongsTo('icingaweb_role', Role::class) // TODO(ak): make 'role' working
->setCandidateKey('role_id')
->setJoinType('INNER');
}
}

@ -0,0 +1,49 @@
<?php
/* Icinga Web 2 | (c) 2024 Icinga GmbH | GPLv2+ */
namespace Icinga\Model;
use ipl\Orm\Behavior\BoolCast;
use ipl\Orm\Behaviors;
use ipl\Orm\Model;
use ipl\Orm\Relations;
/**
* A database model for Icinga Web role-permission table
*
* @property int $role_id Role identifier
* @property string $permission Permission name
* @property bool $allowed Whether the permission is allowed
* @property bool $denied Whether the permission is denied
* @property Role $role Role object
*/
class RolePermission extends Model
{
public function getTableName(): string
{
return 'icingaweb_role_permission';
}
public function getKeyName(): array
{
return ['role_id', 'permission'];
}
public function getColumns(): array
{
return ['allowed', 'denied'];
}
public function createBehaviors(Behaviors $behaviors): void
{
$behaviors->add(new BoolCast(['allowed', 'denied']));
}
public function createRelations(Relations $relations): void
{
$relations->belongsTo('icingaweb_role', Role::class) // TODO(ak): make 'role' working
->setCandidateKey('role_id')
->setJoinType('INNER');
}
}

@ -0,0 +1,41 @@
<?php
/* Icinga Web 2 | (c) 2024 Icinga GmbH | GPLv2+ */
namespace Icinga\Model;
use ipl\Orm\Model;
use ipl\Orm\Relations;
/**
* A database model for Icinga Web role-restriction table
*
* @property int $role_id Role identifier
* @property string $restriction Restriction name
* @property string $filter Filter of things the role is restricted to
* @property Role $role Role object
*/
class RoleRestriction extends Model
{
public function getTableName(): string
{
return 'icingaweb_role_restriction';
}
public function getKeyName(): array
{
return ['role_id', 'restriction'];
}
public function getColumns(): array
{
return ['filter'];
}
public function createRelations(Relations $relations): void
{
$relations->belongsTo('icingaweb_role', Role::class) // TODO(ak): make 'role' working
->setCandidateKey('role_id')
->setJoinType('INNER');
}
}

@ -0,0 +1,40 @@
<?php
/* Icinga Web 2 | (c) 2024 Icinga GmbH | GPLv2+ */
namespace Icinga\Model;
use ipl\Orm\Model;
use ipl\Orm\Relations;
/**
* A database model for Icinga Web role-user table
*
* @property int $role_id Role identifier
* @property string $user_name User name
* @property Role $role Role object
*/
class RoleUser extends Model
{
public function getTableName(): string
{
return 'icingaweb_role_user';
}
public function getKeyName(): array
{
return ['user_name', 'role_id'];
}
public function getColumns(): array
{
return [];
}
public function createRelations(Relations $relations): void
{
$relations->belongsTo('icingaweb_role', Role::class) // TODO(ak): make 'role' working
->setCandidateKey('role_id')
->setJoinType('INNER');
}
}

@ -0,0 +1,101 @@
<?php
/* Icinga Web 2 | (c) 2024 Icinga GmbH | GPLv2+ */
namespace Icinga\Web\Widget;
use Icinga\Web\Url;
use ipl\Html\BaseHtmlElement;
use ipl\Html\Html;
use ipl\I18n\Translation;
/**
* Render roles as table
*/
class RolesTable extends BaseHtmlElement
{
use Translation;
protected $tag = 'table';
/**
* The roles to display
*
* @var iterable
*/
protected $roles = [];
protected function assemble(): void
{
$this->setAttributes([
'class' => 'table-row-selectable common-table',
'data-base-target' => '_next'
]);
$this->addHtml(Html::tag('thead', [], [
Html::tag('tr', [], [
Html::tag('th', [], [$this->translate('Name')]),
Html::tag('th', [], [$this->translate('Users')]),
Html::tag('th', [], [$this->translate('Groups')]),
Html::tag('th', [], [$this->translate('Inherits From')]),
Html::tag('th')
])
]));
$tbody = Html::tag('tbody');
$this->addHtml($tbody);
foreach ($this->roles as $role) {
$users = [];
$groups = [];
foreach ($role->users as $user) {
$users[] = $user->user_name;
}
foreach ($role->groups as $group) {
$groups[] = $group->group_name;
}
sort($users);
sort($groups);
$tbody->addHtml(Html::tag('tr', [], [
Html::tag('td', [], [Html::tag(
'a',
[
'href' => Url::fromPath('role/edit', ['role' => $role->name]),
'title' => sprintf($this->translate('Edit role %s'), $role->name)
],
$role->name
)]),
Html::tag('td', [], [implode(',', $users)]),
Html::tag('td', [], [implode(',', $groups)]),
Html::tag('td', [], $role->parent ? [$role->parent->name] : null),
Html::tag('td', ['class' => 'icon-col'], [Html::tag(
'a',
[
'href' => Url::fromPath('role/remove', ['role' => $role->name]),
'class' => 'action-link icon-cancel',
'title' => sprintf($this->translate('Remove role %s'), $role->name)
]
)])
]));
}
}
/**
* Set the roles to display
*
* @param iterable $roles
*
* @return $this
*/
public function setRoles(iterable $roles): self
{
$this->roles = $roles;
return $this;
}
}

@ -33,6 +33,8 @@ class GeneralConfigPage extends Form
$appConfigForm->createElements($formData);
$appConfigForm->removeElement('global_module_path');
$appConfigForm->removeElement('global_config_resource');
$appConfigForm->removeElement('global_store_roles_in_db');
$this->addElement('hidden', 'global_store_roles_in_db', ['disabled' => true, 'value' => 1]);
$this->addElements($appConfigForm->getElements());
$loggingConfigForm = new LoggingConfigForm();

@ -3,23 +3,29 @@
namespace Icinga\Module\Setup\Steps;
use DateTime;
use Exception;
use Icinga\Application\Config;
use Icinga\Common\Database;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\IcingaException;
use Icinga\Authentication\User\DbUserBackend;
use Icinga\Module\Setup\Step;
use ipl\Sql\Connection;
use ipl\Sql\Insert;
class AuthenticationStep extends Step
{
use Database;
protected $data;
protected $dbError;
protected $authIniError;
protected $permIniError;
protected $roleError;
public function __construct(array $data)
{
@ -29,11 +35,15 @@ class AuthenticationStep extends Step
public function apply()
{
$success = $this->createAuthenticationIni();
if (isset($this->data['adminAccountData']['resourceConfig'])) {
$success &= $this->createAccount();
}
$success &= $this->createRolesIni();
if (isset($this->data['rolesResourceConfig'])) {
$success &= $this->createRoles();
}
return $success;
}
@ -61,34 +71,59 @@ class AuthenticationStep extends Step
return true;
}
protected function createRolesIni()
protected function createRoles(): bool
{
if (isset($this->data['adminAccountData']['username'])) {
$config = array(
'users' => $this->data['adminAccountData']['username'],
'permissions' => '*'
);
if ($this->data['backendConfig']['backend'] === 'db') {
$config['groups'] = mt('setup', 'Administrators', 'setup.role.name');
}
} else { // isset($this->data['adminAccountData']['groupname'])
$config = array(
'groups' => $this->data['adminAccountData']['groupname'],
'permissions' => '*'
);
}
try {
Config::fromArray(array(mt('setup', 'Administrators', 'setup.role.name') => $config))
->setConfigFile(Config::resolvePath('roles.ini'))
->saveIni();
$this->getDb(new ConfigObject($this->data['rolesResourceConfig']))->transaction(function (Connection $db) {
$admins = mt('setup', 'Administrators', 'setup.role.name');
$db->prepexec(
(new Insert())
->into('icingaweb_role')
->columns(['name', 'ctime'])
->values([$admins, (new DateTime())->getTimestamp() * 1000])
);
$id = $db->lastInsertId();
$db->prepexec(
(new Insert())
->into('icingaweb_role_permission')
->columns(['role_id', 'permission', 'allowed'])
->values([$id, '*', 'y'])
);
if (isset($this->data['adminAccountData']['username'])) {
$db->prepexec(
(new Insert())
->into('icingaweb_role_user')
->columns(['role_id', 'user_name'])
->values([$id, $this->data['adminAccountData']['username']])
);
if ($this->data['backendConfig']['backend'] === 'db') {
$db->prepexec(
(new Insert())
->into('icingaweb_role_group')
->columns(['role_id', 'group_name'])
->values([$id, $admins])
);
}
} else {
$db->prepexec(
(new Insert())
->into('icingaweb_role_group')
->columns(['role_id', 'group_name'])
->values([$id, $this->data['adminAccountData']['groupname']])
);
}
});
} catch (Exception $e) {
$this->permIniError = $e;
$this->roleError = $e;
return false;
}
$this->permIniError = false;
$this->roleError = false;
return true;
}
@ -211,7 +246,7 @@ class AuthenticationStep extends Step
$report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->dbError));
}
if ($this->permIniError === false) {
if ($this->roleError === false) {
$report[] = isset($this->data['adminAccountData']['username']) ? sprintf(
mt('setup', 'Account "%s" has been successfully defined as initial administrator.'),
$this->data['adminAccountData']['username']
@ -219,7 +254,7 @@ class AuthenticationStep extends Step
mt('setup', 'The members of the user group "%s" were successfully defined as initial administrators.'),
$this->data['adminAccountData']['groupname']
);
} elseif ($this->permIniError !== null) {
} elseif ($this->roleError !== null) {
$report[] = isset($this->data['adminAccountData']['username']) ? sprintf(
mt('setup', 'Unable to define account "%s" as initial administrator. An error occured:'),
$this->data['adminAccountData']['username']
@ -230,7 +265,7 @@ class AuthenticationStep extends Step
),
$this->data['adminAccountData']['groupname']
);
$report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->permIniError));
$report[] = sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->roleError));
}
return $report;

@ -97,6 +97,11 @@ class WebWizard extends Wizard implements SetupWizard
'icingaweb_group',
'icingaweb_group_membership',
'icingaweb_user',
'icingaweb_role',
'icingaweb_role_user',
'icingaweb_role_group',
'icingaweb_role_permission',
'icingaweb_role_restriction',
'icingaweb_user_preference',
'icingaweb_rememberme',
'icingaweb_schema'
@ -514,11 +519,13 @@ class WebWizard extends Wizard implements SetupWizard
$authType = $pageData['setup_authentication_type']['type'];
$setup->addStep(
new AuthenticationStep(array(
'adminAccountData' => $adminAccountData,
'backendConfig' => $pageData['setup_authentication_backend'],
'resourceName' => $authType === 'db' ? $pageData['setup_auth_db_resource']['name'] : (
'adminAccountData' => $adminAccountData,
'backendConfig' => $pageData['setup_authentication_backend'],
'resourceName' => $authType === 'db' ? $pageData['setup_auth_db_resource']['name'] : (
$authType === 'ldap' ? $pageData['setup_ldap_resource']['name'] : null
)
),
'rolesResourceConfig' => $pageData['setup_auth_db_resource']
?? $pageData['setup_config_db_resource'] ?? null
))
);

@ -0,0 +1,57 @@
CREATE TABLE icingaweb_role (
id int unsigned NOT NULL AUTO_INCREMENT,
parent_id int unsigned DEFAULT NULL,
name varchar(254) NOT NULL,
unrestricted enum('n', 'y') NOT NULL DEFAULT 'n',
ctime bigint unsigned NOT NULL,
mtime bigint unsigned DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_icingaweb_role_parent_id FOREIGN KEY (parent_id)
REFERENCES icingaweb_role (id) ON DELETE SET NULL,
CONSTRAINT idx_icingaweb_role_name UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_user (
role_id int unsigned NOT NULL,
user_name varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (role_id, user_name),
INDEX idx_icingaweb_role_user_user_name (user_name),
CONSTRAINT fk_icingaweb_role_user_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_group (
role_id int unsigned NOT NULL,
group_name varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (role_id, group_name),
INDEX idx_icingaweb_role_group_group_name (group_name),
CONSTRAINT fk_icingaweb_role_group_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_permission (
role_id int unsigned NOT NULL,
permission varchar(254) NOT NULL,
allowed enum('n', 'y') NOT NULL DEFAULT 'n',
denied enum('n', 'y') NOT NULL DEFAULT 'n',
PRIMARY KEY (role_id, permission),
CONSTRAINT fk_icingaweb_role_permission_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_restriction (
role_id int unsigned NOT NULL,
restriction varchar(254) NOT NULL,
filter text NOT NULL,
PRIMARY KEY (role_id, restriction),
CONSTRAINT fk_icingaweb_role_restriction_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO icingaweb_schema (version, timestamp, success, reason)
VALUES('2.13.0', UNIX_TIMESTAMP() * 1000, 'y', NULL);

@ -31,6 +31,61 @@ CREATE TABLE `icingaweb_user`(
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
CREATE TABLE icingaweb_role (
id int unsigned NOT NULL AUTO_INCREMENT,
parent_id int unsigned DEFAULT NULL,
name varchar(254) NOT NULL,
unrestricted enum('n', 'y') NOT NULL DEFAULT 'n',
ctime bigint unsigned NOT NULL,
mtime bigint unsigned DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_icingaweb_role_parent_id FOREIGN KEY (parent_id)
REFERENCES icingaweb_role (id) ON DELETE SET NULL,
CONSTRAINT idx_icingaweb_role_name UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_user (
role_id int unsigned NOT NULL,
user_name varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (role_id, user_name),
INDEX idx_icingaweb_role_user_user_name (user_name),
CONSTRAINT fk_icingaweb_role_user_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_group (
role_id int unsigned NOT NULL,
group_name varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (role_id, group_name),
INDEX idx_icingaweb_role_group_group_name (group_name),
CONSTRAINT fk_icingaweb_role_group_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_permission (
role_id int unsigned NOT NULL,
permission varchar(254) NOT NULL,
allowed enum('n', 'y') NOT NULL DEFAULT 'n',
denied enum('n', 'y') NOT NULL DEFAULT 'n',
PRIMARY KEY (role_id, permission),
CONSTRAINT fk_icingaweb_role_permission_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE icingaweb_role_restriction (
role_id int unsigned NOT NULL,
restriction varchar(254) NOT NULL,
filter text NOT NULL,
PRIMARY KEY (role_id, restriction),
CONSTRAINT fk_icingaweb_role_restriction_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `icingaweb_user_preference`(
`username` varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
`section` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
@ -65,4 +120,4 @@ CREATE TABLE icingaweb_schema (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
INSERT INTO icingaweb_schema (version, timestamp, success)
VALUES ('2.12.0', UNIX_TIMESTAMP() * 1000, 'y');
VALUES ('2.13.0', UNIX_TIMESTAMP() * 1000, 'y');

@ -0,0 +1,59 @@
CREATE TABLE icingaweb_role (
id serial,
parent_id int DEFAULT NULL,
name varchar(254) NOT NULL,
unrestricted boolenum NOT NULL DEFAULT 'n',
ctime bigint NOT NULL,
mtime bigint DEFAULT NULL,
CONSTRAINT pk_icingaweb_role PRIMARY KEY (id),
CONSTRAINT fk_icingaweb_role_parent_id FOREIGN KEY (parent_id)
REFERENCES icingaweb_role (id) ON DELETE SET NULL,
CONSTRAINT idx_icingaweb_role_name UNIQUE (name)
);
CREATE TABLE icingaweb_role_user (
role_id int NOT NULL,
user_name citext NOT NULL,
CONSTRAINT pk_icingaweb_role_user PRIMARY KEY (role_id, user_name),
CONSTRAINT fk_icingaweb_role_user_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
CREATE INDEX idx_icingaweb_role_user_user_name ON icingaweb_role_user(user_name);
CREATE TABLE icingaweb_role_group (
role_id int NOT NULL,
group_name citext NOT NULL,
CONSTRAINT pk_icingaweb_role_group PRIMARY KEY (role_id, group_name),
CONSTRAINT fk_icingaweb_role_group_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
CREATE INDEX idx_icingaweb_role_group_group_name ON icingaweb_role_group(group_name);
CREATE TABLE icingaweb_role_permission (
role_id int NOT NULL,
permission varchar(254) NOT NULL,
allowed boolenum NOT NULL DEFAULT 'n',
denied boolenum NOT NULL DEFAULT 'n',
CONSTRAINT pk_icingaweb_role_permission PRIMARY KEY (role_id, permission),
CONSTRAINT fk_icingaweb_role_permission_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
CREATE TABLE icingaweb_role_restriction (
role_id int NOT NULL,
restriction varchar(254) NOT NULL,
filter text NOT NULL,
CONSTRAINT pk_icingaweb_role_restriction PRIMARY KEY (role_id, restriction),
CONSTRAINT fk_icingaweb_role_restriction_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
INSERT INTO icingaweb_schema (version, timestamp, success, reason)
VALUES('2.13.0', EXTRACT(EPOCH FROM now()) * 1000, 'y', NULL);

@ -1,5 +1,7 @@
/* Icinga Web 2 | (c) 2014 Icinga GmbH | GPLv2+ */
CREATE TYPE boolenum AS ENUM ('n', 'y');
CREATE OR REPLACE FUNCTION unix_timestamp(timestamp with time zone) RETURNS bigint AS '
SELECT EXTRACT(EPOCH FROM $1)::bigint AS result
' LANGUAGE sql;
@ -76,6 +78,63 @@ CREATE UNIQUE INDEX idx_icingaweb_user
lower((name)::text)
);
CREATE TABLE icingaweb_role (
id serial,
parent_id int DEFAULT NULL,
name varchar(254) NOT NULL,
unrestricted boolenum NOT NULL DEFAULT 'n',
ctime bigint NOT NULL,
mtime bigint DEFAULT NULL,
CONSTRAINT pk_icingaweb_role PRIMARY KEY (id),
CONSTRAINT fk_icingaweb_role_parent_id FOREIGN KEY (parent_id)
REFERENCES icingaweb_role (id) ON DELETE SET NULL,
CONSTRAINT idx_icingaweb_role_name UNIQUE (name)
);
CREATE TABLE icingaweb_role_user (
role_id int NOT NULL,
user_name citext NOT NULL,
CONSTRAINT pk_icingaweb_role_user PRIMARY KEY (role_id, user_name),
CONSTRAINT fk_icingaweb_role_user_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
CREATE INDEX idx_icingaweb_role_user_user_name ON icingaweb_role_user(user_name);
CREATE TABLE icingaweb_role_group (
role_id int NOT NULL,
group_name citext NOT NULL,
CONSTRAINT pk_icingaweb_role_group PRIMARY KEY (role_id, group_name),
CONSTRAINT fk_icingaweb_role_group_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
CREATE INDEX idx_icingaweb_role_group_group_name ON icingaweb_role_group(group_name);
CREATE TABLE icingaweb_role_permission (
role_id int NOT NULL,
permission varchar(254) NOT NULL,
allowed boolenum NOT NULL DEFAULT 'n',
denied boolenum NOT NULL DEFAULT 'n',
CONSTRAINT pk_icingaweb_role_permission PRIMARY KEY (role_id, permission),
CONSTRAINT fk_icingaweb_role_permission_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
CREATE TABLE icingaweb_role_restriction (
role_id int NOT NULL,
restriction varchar(254) NOT NULL,
filter text NOT NULL,
CONSTRAINT pk_icingaweb_role_restriction PRIMARY KEY (role_id, restriction),
CONSTRAINT fk_icingaweb_role_restriction_role_id FOREIGN KEY (role_id)
REFERENCES icingaweb_role (id) ON DELETE CASCADE
);
CREATE TABLE "icingaweb_user_preference" (
"username" character varying(254) NOT NULL,
"name" character varying(64) NOT NULL,
@ -118,8 +177,6 @@ ALTER TABLE ONLY "icingaweb_rememberme"
"id"
);
CREATE TYPE boolenum AS ENUM ('n', 'y');
CREATE TABLE "icingaweb_schema" (
"id" serial,
"version" varchar(64) NOT NULL,
@ -132,4 +189,4 @@ CREATE TABLE "icingaweb_schema" (
);
INSERT INTO icingaweb_schema (version, timestamp, success)
VALUES ('2.12.0', extract(epoch from now()) * 1000, 'y');
VALUES ('2.13.0', extract(epoch from now()) * 1000, 'y');