diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:44:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:44:46 +0000 |
commit | b18bc644404e02b57635bfcc8258e85abb141146 (patch) | |
tree | 686512eacb2dba0055277ef7ec2f28695b3418ea /application/clicommands/MigrateCommand.php | |
parent | Initial commit. (diff) | |
download | icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.tar.xz icingadb-web-b18bc644404e02b57635bfcc8258e85abb141146.zip |
Adding upstream version 1.1.1.upstream/1.1.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'application/clicommands/MigrateCommand.php')
-rw-r--r-- | application/clicommands/MigrateCommand.php | 835 |
1 files changed, 835 insertions, 0 deletions
diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php new file mode 100644 index 0000000..6d034ee --- /dev/null +++ b/application/clicommands/MigrateCommand.php @@ -0,0 +1,835 @@ +<?php + +/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Icingadb\Clicommands; + +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Cli\Command; +use Icinga\Data\ConfigObject; +use Icinga\Exception\NotReadableError; +use Icinga\Exception\NotWritableError; +use Icinga\Module\Icingadb\Compat\UrlMigrator; +use Icinga\Util\DirectoryIterator; +use Icinga\Web\Request; +use ipl\Stdlib\Str; +use ipl\Web\Filter\QueryString; +use ipl\Web\Url; + +class MigrateCommand extends Command +{ + /** @var bool Skip the migration, only perform transformations */ + protected $skipMigration = false; + + public function init(): void + { + Logger::getInstance()->setLevel(Logger::INFO); + } + + /** + * Migrate monitoring navigation items to Icinga DB Web + * + * USAGE + * + * icingacli icingadb migrate navigation [options] + * + * REQUIRED OPTIONS: + * + * --user=<name> Migrate navigation items whose owner matches the given + * name or owners matching the given pattern. Wildcard + * matching by `*` possible. + * + * OPTIONS: + * + * --override Replace existing or already migrated items + * (Attention: Actions are not backed up) + * + * --no-backup Remove monitoring actions and don't back up menu items + */ + public function navigationAction(): void + { + /** @var string $user */ + $user = $this->params->getRequired('user'); + $noBackup = $this->params->get('no-backup'); + + $preferencesPath = Config::resolvePath('preferences'); + $sharedNavigation = Config::resolvePath('navigation'); + if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) { + Logger::info('There are no user navigation items to migrate'); + return; + } + + $rc = 0; + $directories = file_exists($preferencesPath) ? new DirectoryIterator($preferencesPath) : []; + + $anythingChanged = false; + + /** @var string $directory */ + foreach ($directories as $directory) { + /** @var string $username */ + $username = $directories->key() === false ? '' : $directories->key(); + if (fnmatch($user, $username) === false) { + continue; + } + + $menuItems = $this->readFromIni($directory . '/menu.ini', $rc); + $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc); + $icingadbHostActions = $this->readFromIni($directory . '/icingadb-host-actions.ini', $rc); + $icingadbServiceActions = $this->readFromIni($directory . '/icingadb-service-actions.ini', $rc); + + $menuUpdated = false; + $originalMenuItems = $this->readFromIni($directory . '/menu.ini', $rc); + + Logger::info( + 'Transforming legacy wildcard filters of existing Icinga DB Web items for user "%s"', + $username + ); + + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->transformNavigationItems($menuItems, $username, $rc); + $anythingChanged |= $menuUpdated; + } + + if (! $icingadbHostActions->isEmpty()) { + $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $username, $rc); + } + + if (! $icingadbServiceActions->isEmpty()) { + $anythingChanged |= $this->transformNavigationItems( + $icingadbServiceActions, + $username, + $rc + ); + } + + if (! $this->skipMigration) { + Logger::info('Migrating monitoring navigation items for user "%s" to Icinga DB Web', $username); + + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->migrateNavigationItems($menuItems, $username, $directory . '/menu.ini', $rc); + $anythingChanged |= $menuUpdated; + } + + if (! $hostActions->isEmpty()) { + $anythingChanged |= $this->migrateNavigationItems( + $hostActions, + $username, + $directory . '/icingadb-host-actions.ini', + $rc + ); + } + + if (! $serviceActions->isEmpty()) { + $anythingChanged |= $this->migrateNavigationItems( + $serviceActions, + $username, + $directory . '/icingadb-service-actions.ini', + $rc + ); + } + } + + if ($menuUpdated && ! $noBackup) { + $this->createBackupIni("$directory/menu", $originalMenuItems); + } + } + + // Start migrating shared navigation items + $menuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc); + $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc); + $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc); + $icingadbHostActions = $this->readFromIni($sharedNavigation . '/icingadb-host-actions.ini', $rc); + $icingadbServiceActions = $this->readFromIni($sharedNavigation . '/icingadb-service-actions.ini', $rc); + + $menuUpdated = false; + $originalMenuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc); + + Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web navigation items'); + + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->transformNavigationItems($menuItems, $user, $rc); + $anythingChanged |= $menuUpdated; + } + + if (! $icingadbHostActions->isEmpty()) { + $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $user, $rc); + } + + if (! $icingadbServiceActions->isEmpty()) { + $anythingChanged |= $this->transformNavigationItems( + $icingadbServiceActions, + $user, + $rc + ); + } + + if (! $this->skipMigration) { + Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web items'); + + if (! $menuItems->isEmpty()) { + $menuUpdated = $this->migrateNavigationItems($menuItems, $user, $sharedNavigation . '/menu.ini', $rc); + $anythingChanged |= $menuUpdated; + } + + if (! $hostActions->isEmpty()) { + $anythingChanged |= $this->migrateNavigationItems( + $hostActions, + $user, + $sharedNavigation . '/icingadb-host-actions.ini', + $rc + ); + } + + if (! $serviceActions->isEmpty()) { + $anythingChanged |= $this->migrateNavigationItems( + $serviceActions, + $user, + $sharedNavigation . '/icingadb-service-actions.ini', + $rc + ); + } + } + + if ($menuUpdated && ! $noBackup) { + $this->createBackupIni("$sharedNavigation/menu", $originalMenuItems); + } + + if ($rc > 0) { + if ($this->skipMigration) { + Logger::error('Failed to transform some icingadb navigation items'); + } else { + Logger::error('Failed to migrate some monitoring navigation items'); + } + + exit($rc); + } + + if (! $anythingChanged) { + Logger::info('Nothing to do'); + } elseif ($this->skipMigration) { + Logger::info('Successfully transformed all icingadb navigation item filters'); + } else { + Logger::info('Successfully migrated all monitoring navigation items'); + } + } + + + /** + * Migrate monitoring restrictions and permissions to Icinga DB Web + * + * Migrated roles do not grant general or full access to users afterward. + * It is recommended to review any changes made by this command, before + * manually granting access. + * + * USAGE + * + * icingacli icingadb migrate role [options] + * + * REQUIRED OPTIONS: (Use either, not both) + * + * --group=<name> Update roles that are assigned to the given group or to + * groups matching the pattern. Wildcard matching by `*` + * possible. + * + * --role=<name> Update role with the given name or roles whose names + * match the pattern. Wildcard matching by `*` possible. + * + * OPTIONS: + * + * --override Reset any existing Icinga DB Web rules + * + * --no-backup Don't back up roles + */ + public function roleAction(): void + { + /** @var ?bool $override */ + $override = $this->params->get('override'); + $noBackup = $this->params->get('no-backup'); + + /** @var ?string $groupName */ + $groupName = $this->params->get('group'); + /** @var ?string $roleName */ + $roleName = $this->params->get('role'); + + if ($roleName === null && $groupName === null) { + $this->fail("One of the parameters 'group' or 'role' must be supplied"); + } elseif ($roleName !== null && $groupName !== null) { + $this->fail("Use either 'group' or 'role'. Both cannot be used as role overrules group."); + } + + $rc = 0; + $changed = false; + + $restrictions = Config::$configDir . '/roles.ini'; + $rolesConfig = $this->readFromIni($restrictions, $rc); + $monitoringRestriction = 'monitoring/filter/objects'; + $monitoringPropertyBlackList = 'monitoring/blacklist/properties'; + $icingadbRestrictions = [ + 'objects' => 'icingadb/filter/objects', + 'hosts' => 'icingadb/filter/hosts', + 'services' => 'icingadb/filter/services' + ]; + + $icingadbPropertyDenyList = 'icingadb/denylist/variables'; + foreach ($rolesConfig as $name => $role) { + /** @var string[] $role */ + $role = iterator_to_array($role); + + if ($roleName === '*' || $groupName === '*') { + $roleMatch = true; + } elseif ($roleName !== null && fnmatch($roleName, $name)) { + $roleMatch = true; + } elseif ($groupName !== null && isset($role['groups'])) { + $roleGroups = array_map('trim', explode(',', $role['groups'])); + $roleMatch = false; + foreach ($roleGroups as $roleGroup) { + if (fnmatch($groupName, $roleGroup)) { + $roleMatch = true; + break; + } + } + } else { + $roleMatch = false; + } + + if ($roleMatch && ! $this->skipMigration && $this->shouldUpdateRole($role, $override)) { + if (isset($role[$monitoringRestriction])) { + Logger::info( + 'Migrating monitoring restriction filter for role "%s" to the Icinga DB Web restrictions', + $name + ); + $transformedFilter = UrlMigrator::transformFilter( + QueryString::parse($role[$monitoringRestriction]) + ); + + if ($transformedFilter) { + $role[$icingadbRestrictions['objects']] = QueryString::render($transformedFilter); + $changed = true; + } + } + + if (isset($role[$monitoringPropertyBlackList])) { + Logger::info( + 'Migrating monitoring blacklisted properties for role "%s" to the Icinga DB Web deny list', + $name + ); + + $icingadbProperties = []; + foreach (explode(',', $role[$monitoringPropertyBlackList]) as $property) { + $icingadbProperties[] = preg_replace('/^(?:host|service)\.vars\./i', '', $property, 1); + } + + $role[$icingadbPropertyDenyList] = str_replace( + '**', + '*', + implode(',', array_unique($icingadbProperties)) + ); + + $changed = true; + } + + if (isset($role['permissions'])) { + $updatedPermissions = []; + Logger::info( + 'Migrating monitoring permissions for role "%s" to the Icinga DB Web permissions', + $name + ); + + if (strpos($role['permissions'], 'monitoring')) { + $monitoringProtection = Config::module('monitoring') + ->get('security', 'protected_customvars'); + + if ($monitoringProtection !== null) { + $role['icingadb/protect/variables'] = $monitoringProtection; + $changed = true; + } + } + + foreach (explode(',', $role['permissions']) as $permission) { + if (Str::startsWith($permission, 'icingadb/') || $permission === 'module/icingadb') { + continue; + } elseif (Str::startsWith($permission, 'monitoring/command/')) { + $changed = true; + $updatedPermissions[] = $permission; + $updatedPermissions[] = str_replace('monitoring/', 'icingadb/', $permission); + } elseif ($permission === 'no-monitoring/contacts') { + $changed = true; + $updatedPermissions[] = $permission; + $role['icingadb/denylist/routes'] = 'users,usergroups'; + } else { + $updatedPermissions[] = $permission; + } + } + + $role['permissions'] = implode(',', $updatedPermissions); + } + + if (isset($role['refusals']) && is_string($role['refusals'])) { + $updatedRefusals = []; + Logger::info( + 'Migrating monitoring refusals for role "%s" to the Icinga DB Web refusals', + $name + ); + + foreach (explode(',', $role['refusals']) as $refusal) { + if (Str::startsWith($refusal, 'icingadb/') || $refusal === 'module/icingadb') { + continue; + } elseif (Str::startsWith($refusal, 'monitoring/command/')) { + $changed = true; + $updatedRefusals[] = $refusal; + $updatedRefusals[] = str_replace('monitoring/', 'icingadb/', $refusal); + } else { + $updatedRefusals[] = $refusal; + } + } + + $role['refusals'] = implode(',', $updatedRefusals); + } + } + + if ($roleMatch) { + foreach ($icingadbRestrictions as $object => $icingadbRestriction) { + if (isset($role[$icingadbRestriction]) && is_string($role[$icingadbRestriction])) { + $filter = QueryString::parse($role[$icingadbRestriction]); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); + + if ($filter) { + $filter = QueryString::render($filter); + if ($filter !== $role[$icingadbRestriction]) { + Logger::info( + 'Icinga Db Web restriction of role "%s" for %s changed from "%s" to "%s"', + $name, + $object, + $role[$icingadbRestriction], + $filter + ); + + $role[$icingadbRestriction] = $filter; + $changed = true; + } + } + } + } + } + + $rolesConfig->setSection($name, $role); + } + + if ($changed) { + if (! $noBackup) { + $this->createBackupIni(Config::$configDir . '/roles'); + } + + try { + $rolesConfig->saveIni(); + } catch (NotWritableError $error) { + Logger::error($error); + if ($this->skipMigration) { + Logger::error('Failed to transform icingadb restrictions'); + } else { + Logger::error('Failed to migrate monitoring restrictions'); + } + + exit(256); + } + + if ($this->skipMigration) { + Logger::info('Successfully transformed all icingadb restrictions'); + } else { + Logger::info('Successfully migrated monitoring restrictions and permissions in roles'); + } + } else { + Logger::info('Nothing to do'); + } + } + + /** + * Migrate monitoring dashboards to Icinga DB Web + * + * USAGE + * + * icingacli icingadb migrate dashboard [options] + * + * REQUIRED OPTIONS: + * + * --user=<name> Migrate dashboards whose owner matches the given + * name or owners matching the given pattern. Wildcard + * matching by `*` possible. + * + * OPTIONS: + * + * --no-backup Don't back up dashboards + */ + public function dashboardAction(): void + { + /** @var string $user */ + $user = $this->params->getRequired('user'); + $noBackup = $this->params->get('no-backup'); + + $dashboardsPath = Config::resolvePath('dashboards'); + if (! file_exists($dashboardsPath)) { + Logger::info('There are no dashboards to migrate'); + return; + } + + $rc = 0; + $directories = new DirectoryIterator($dashboardsPath); + + $anythingChanged = false; + + /** @var string $directory */ + foreach ($directories as $directory) { + /** @var string $userName */ + $userName = $directories->key() === false ? '' : $directories->key(); + if (fnmatch($user, $userName) === false) { + continue; + } + + $dashboardsConfig = $this->readFromIni($directory . '/dashboard.ini', $rc); + $backupConfig = $this->readFromIni($directory . '/dashboard.ini', $rc); + + Logger::info( + 'Migrating monitoring dashboards to Icinga DB Web dashboards for user "%s"', + $userName + ); + + $changed = false; + /** @var ConfigObject $dashboardConfig */ + foreach ($dashboardsConfig->getConfigObject() as $name => $dashboardConfig) { + /** @var ?string $dashboardUrlString */ + $dashboardUrlString = $dashboardConfig->get('url'); + if ($dashboardUrlString !== null) { + $dashBoardUrl = Url::fromPath($dashboardUrlString, [], new Request()); + if (! $this->skipMigration && Str::startsWith(ltrim($dashboardUrlString, '/'), 'monitoring/')) { + $dashboardConfig->url = UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl(); + $changed = true; + } + + if (Str::startsWith(ltrim($dashboardUrlString, '/'), 'icingadb/')) { + $finalUrl = $dashBoardUrl->onlyWith(['sort', 'limit', 'view', 'columns', 'page']); + $params = $dashBoardUrl->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); + $filter = QueryString::parse($params->toString()); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); + if ($filter) { + $oldFilterString = $params->toString(); + $newFilterString = QueryString::render($filter); + + if ($oldFilterString !== $newFilterString) { + Logger::info( + 'Icinga Db Web filter of dashboard "%s" has changed from "%s" to "%s"', + $name, + $params->toString(), + QueryString::render($filter) + ); + $finalUrl->setFilter($filter); + + $dashboardConfig->url = $finalUrl->getRelativeUrl(); + $changed = true; + } + } + } + } + } + + + if ($changed && $noBackup === null) { + $this->createBackupIni("$directory/dashboard", $backupConfig); + } + + if ($changed) { + $anythingChanged = true; + } + + try { + $dashboardsConfig->saveIni(); + } catch (NotWritableError $error) { + Logger::error($error); + $rc = 256; + } + } + + if ($rc > 0) { + if ($this->skipMigration) { + Logger::error('Failed to transform some icingadb dashboards'); + } else { + Logger::error('Failed to migrate some monitoring dashboards'); + } + + exit($rc); + } + + if (! $anythingChanged) { + Logger::info('Nothing to do'); + } elseif ($this->skipMigration) { + Logger::info('Successfully transformed all icingadb dashboards'); + } else { + Logger::info('Successfully migrated dashboards for all the matched users'); + } + } + + /** + * Migrate Icinga DB Web wildcard filters of navigation items, dashboards and roles + * + * USAGE + * + * icingacli icingadb migrate filter + * + * OPTIONS: + * + * --no-backup Don't back up menu items, dashboards and roles + */ + public function filterAction(): void + { + $this->skipMigration = true; + + $this->params->set('user', '*'); + $this->navigationAction(); + $this->dashboardAction(); + + $this->params->set('role', '*'); + $this->roleAction(); + } + + private function transformNavigationItems(Config $config, string $owner, int &$rc): bool + { + $updated = false; + /** @var ConfigObject $newConfigObject */ + foreach ($config->getConfigObject() as $section => $newConfigObject) { + /** @var string $configOwner */ + $configOwner = $newConfigObject->get('owner') ?? ''; + if ($configOwner && $configOwner !== $owner) { + continue; + } + + if ( + $newConfigObject->get('type') === 'icingadb-host-action' + || $newConfigObject->get('type') === 'icingadb-service-action' + ) { + /** @var ?string $legacyFilter */ + $legacyFilter = $newConfigObject->get('filter'); + if ($legacyFilter !== null) { + $filter = QueryString::parse($legacyFilter); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); + if ($filter) { + $filter = QueryString::render($filter); + if ($legacyFilter !== $filter) { + $newConfigObject->filter = $filter; + $updated = true; + Logger::info( + 'Icinga DB Web filter of action "%s" is changed from %s to "%s"', + $section, + $legacyFilter, + $filter + ); + } + } + } + } + + /** @var string $url */ + $url = $newConfigObject->get('url'); + if ($url && Str::startsWith(ltrim($url, '/'), 'icingadb/')) { + $url = Url::fromPath($url, [], new Request()); + $finalUrl = $url->onlyWith(['sort', 'limit', 'view', 'columns', 'page']); + $params = $url->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); + $filter = QueryString::parse($params->toString()); + $filter = UrlMigrator::transformLegacyWildcardFilter($filter); + if ($filter) { + $oldFilterString = $params->toString(); + $newFilterString = QueryString::render($filter); + + if ($oldFilterString !== $newFilterString) { + Logger::info( + 'Icinga Db Web filter of navigation item "%s" has changed from "%s" to "%s"', + $section, + $oldFilterString, + $newFilterString + ); + + $newConfigObject->url = $finalUrl->setFilter($filter)->getRelativeUrl(); + $updated = true; + } + } + } + } + + if ($updated) { + try { + $config->saveIni(); + } catch (NotWritableError $error) { + Logger::error($error); + $rc = 256; + + return false; + } + } + + return $updated; + } + + /** + * Migrate the given config to the given new config path + * + * @param Config $config + * @param string $owner + * @param string $path + * @param int $rc + * + * @return bool + */ + private function migrateNavigationItems(Config $config, string $owner, string $path, int &$rc): bool + { + $deleteLegacyFiles = $this->params->get('no-backup'); + $override = $this->params->get('override'); + $newConfig = $config->getConfigFile() === $path ? $config : $this->readFromIni($path, $rc); + + $updated = false; + /** @var ConfigObject $configObject */ + foreach ($config->getConfigObject() as $configObject) { + /** @var string $configOwner */ + $configOwner = $configObject->get('owner') ?? ''; + if ($configOwner && $configOwner !== $owner) { + continue; + } + + $migrateFilter = false; + if ($configObject->type === 'host-action') { + $updated = true; + $migrateFilter = true; + $configObject->type = 'icingadb-host-action'; + } elseif ($configObject->type === 'service-action') { + $updated = true; + $migrateFilter = true; + $configObject->type = 'icingadb-service-action'; + } + + /** @var ?string $urlString */ + $urlString = $configObject->get('url'); + if ($urlString !== null) { + $urlString = str_replace( + ['$SERVICEDESC$', '$HOSTNAME$', '$HOSTADDRESS$', '$HOSTADDRESS6$'], + ['$service.name$', '$host.name$', '$host.address$', '$host.address6$'], + $urlString + ); + if ($urlString !== $configObject->url) { + $configObject->url = $urlString; + $updated = true; + } + + $url = Url::fromPath($urlString, [], new Request()); + + try { + $urlString = UrlMigrator::transformUrl($url)->getRelativeUrl(); + $configObject->url = $urlString; + $updated = true; + } catch (\InvalidArgumentException $err) { + // Do nothing + } + } + + /** @var ?string $legacyFilter */ + $legacyFilter = $configObject->get('filter'); + if ($migrateFilter && $legacyFilter) { + $updated = true; + $filter = QueryString::parse($legacyFilter); + $filter = UrlMigrator::transformFilter($filter); + if ($filter !== false) { + $configObject->filter = QueryString::render($filter); + } else { + unset($configObject->filter); + } + } + + $section = $config->key(); + if (! $newConfig->hasSection($section) || $newConfig === $config || $override) { + $newConfig->setSection($section, $configObject); + } + } + + if ($updated) { + try { + $newConfig->saveIni(); + + // Remove the legacy file only if explicitly requested + if ($deleteLegacyFiles && $newConfig !== $config) { + unlink($config->getConfigFile()); + } + } catch (NotWritableError $error) { + Logger::error($error); + $rc = 256; + + return false; + } + } + + return $updated; + } + + /** + * Get the navigation items config from the given ini path + * + * @param string $path Absolute path of the ini file + * @param int $rc The return code used to exit the action + * + * @return Config + */ + private function readFromIni($path, &$rc) + { + try { + $config = Config::fromIni($path); + } catch (NotReadableError $error) { + Logger::error($error); + + $config = new Config(); + $rc = 128; + } + + return $config; + } + + private function createBackupIni(string $path, Config $config = null): void + { + $counter = 0; + while (true) { + $filepath = $counter > 0 + ? "$path.backup$counter.ini" + : "$path.backup.ini"; + + if (! file_exists($filepath)) { + if ($config) { + $config->saveIni($filepath); + } else { + copy("$path.ini", $filepath); + } + + break; + } else { + $counter++; + } + } + } + + /** + * Checks if the given role should be updated + * + * @param string[] $role + * @param bool $override + * + * @return bool + */ + private function shouldUpdateRole(array $role, ?bool $override): bool + { + return ! ( + isset($role['icingadb/filter/objects']) + || isset($role['icingadb/filter/hosts']) + || isset($role['icingadb/filter/services']) + || isset($role['icingadb/denylist/routes']) + || isset($role['icingadb/denylist/variables']) + || isset($role['icingadb/protect/variables']) + || (isset($role['permissions']) && str_contains($role['permissions'], 'icingadb')) + ) + || $override; + } +} |