From d7e1dd6f79a5e6b7d252b73141a02187a61c748a Mon Sep 17 00:00:00 2001 From: Michael Rodrigues Date: Mon, 13 Jul 2026 12:13:18 -0300 Subject: [PATCH 1/6] Update Update to better manage access to the dashboards. --- front/itemright.form.php | 125 ++++++++++++ inc/dashboard.class.php | 61 +++++- inc/itemright.class.php | 405 +++++++++++++++++++++++++++++++++++++++ setup.php | 5 +- 4 files changed, 589 insertions(+), 7 deletions(-) create mode 100644 front/itemright.form.php create mode 100644 inc/itemright.class.php diff --git a/front/itemright.form.php b/front/itemright.form.php new file mode 100644 index 0000000..ede1891 --- /dev/null +++ b/front/itemright.form.php @@ -0,0 +1,125 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2018-2023 by Metabase plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/metabase + * ------------------------------------------------------------------------- + * + * CUSTOM FORK NOTICE: added to handle per Group / per User dashboard rights. + * ------------------------------------------------------------------------- + */ + +function plugin_metabase_check_itemright_access($itemtype) +{ + if (!in_array($itemtype, PluginMetabaseItemright::SUPPORTED_ITEMTYPES, true)) { + Session::addMessageAfterRedirect( + __s('Invalid request.', 'metabase'), + false, + ERROR, + ); + Html::back(); + } + + if ($itemtype === Group::class) { + Session::checkRight('group', UPDATE); + } else { + Session::checkRight('user', UPDATE); + } +} + +if (isset($_REQUEST['update'])) { + if ( + !array_key_exists('itemtype', $_REQUEST) + || empty($_REQUEST['itemtype']) + || !array_key_exists('items_id', $_REQUEST) + || empty($_REQUEST['items_id']) + || !array_key_exists('dashboard', $_REQUEST) + || !is_array($_REQUEST['dashboard']) + ) { + Session::addMessageAfterRedirect( + __s('Invalid request.', 'metabase'), + false, + ERROR, + ); + Html::back(); + } + + plugin_metabase_check_itemright_access($_REQUEST['itemtype']); + + $viewableDashboardsUuids = []; + foreach ($_REQUEST['dashboard'] as $dashboardUuid => $rights) { + PluginMetabaseItemright::setDashboardRightsForItem( + $_REQUEST['itemtype'], + $_REQUEST['items_id'], + $dashboardUuid, + $rights, + ); + + if (($rights & READ) !== 0) { + $viewableDashboardsUuids[] = $dashboardUuid; + } + } + + if (!empty($viewableDashboardsUuids)) { + $apiclient = new PluginMetabaseAPIClient(); + $apiclient->enableDashboardsEmbeddedDisplay($viewableDashboardsUuids); + } +} elseif (isset($_REQUEST['set_rights_to_all'])) { + if ( + !array_key_exists('itemtype', $_REQUEST) + || empty($_REQUEST['itemtype']) + || !array_key_exists('items_id', $_REQUEST) + || empty($_REQUEST['items_id']) + ) { + Session::addMessageAfterRedirect( + __s('Invalid request.', 'metabase'), + false, + ERROR, + ); + Html::back(); + } + + plugin_metabase_check_itemright_access($_REQUEST['itemtype']); + + $apiclient = new PluginMetabaseAPIClient(); + + $viewableDashboardsUuids = []; + foreach ($apiclient->getDashboards() as $dashboard) { + PluginMetabaseItemright::setDashboardRightsForItem( + $_REQUEST['itemtype'], + $_REQUEST['items_id'], + $dashboard['id'], + $_REQUEST['set_rights_to_all'], + ); + + $viewableDashboardsUuids[] = $dashboard['id']; + } + + if ((int) $_REQUEST['set_rights_to_all'] !== 0) { + $apiclient->enableDashboardsEmbeddedDisplay($viewableDashboardsUuids); + } +} + +Html::back(); diff --git a/inc/dashboard.class.php b/inc/dashboard.class.php index 1a90775..d9fca74 100644 --- a/inc/dashboard.class.php +++ b/inc/dashboard.class.php @@ -43,6 +43,58 @@ public static function getTypeName($nb = 0) return __s('Metabase dashboard', 'metabase'); } + /** + * Check if the currently logged-in user is able to view at least one + * dashboard, combining profile, group and user based rights. + * + * CUSTOM FORK: rights are additive (OR) across profile / groups / user. + * + * @return boolean + */ + public static function canCurrentUserViewDashboards(): bool + { + if (PluginMetabaseProfileright::canProfileViewDashboards($_SESSION['glpiactiveprofile']['id'])) { + return true; + } + + if (PluginMetabaseItemright::canItemViewDashboards(User::class, Session::getLoginUserID())) { + return true; + } + + if (PluginMetabaseItemright::canGroupsViewDashboards($_SESSION['glpigroups'] ?? [])) { + return true; + } + + return false; + } + + /** + * Check if the currently logged-in user is able to view the given + * dashboard, combining profile, group and user based rights. + * + * CUSTOM FORK: rights are additive (OR) across profile / groups / user. + * + * @param integer $dashboardUuid + * + * @return boolean + */ + public static function canCurrentUserViewDashboard($dashboardUuid): bool + { + if (PluginMetabaseProfileright::canProfileViewDashboard($_SESSION['glpiactiveprofile']['id'], $dashboardUuid)) { + return true; + } + + if (PluginMetabaseItemright::canItemViewDashboard(User::class, Session::getLoginUserID(), $dashboardUuid)) { + return true; + } + + if (PluginMetabaseItemright::canGroupsViewDashboard($_SESSION['glpigroups'] ?? [], $dashboardUuid)) { + return true; + } + + return false; + } + /** * {@inheritDoc} * @see CommonGLPI::getTabNameForItem() @@ -51,7 +103,7 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { switch ($item->getType()) { case 'Central': - if (PluginMetabaseProfileright::canProfileViewDashboards($_SESSION['glpiactiveprofile']['id'])) { + if (self::canCurrentUserViewDashboards()) { return self::createTabEntry(self::getTypeName(), 0, $item::getType(), PluginMetabaseConfig::getIcon()); } @@ -69,7 +121,7 @@ public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $ { switch (get_class($item)) { case Central::class: - if (PluginMetabaseProfileright::canProfileViewDashboards($_SESSION['glpiactiveprofile']['id'])) { + if (self::canCurrentUserViewDashboards()) { self::showForCentral($item, $withtemplate); } @@ -99,10 +151,7 @@ public static function showForCentral(Central $item, $withtemplate = 0, $is_help $dashboards, function ($dashboard) { $isEmbeddingEnabled = $dashboard['enable_embedding']; - $canView = PluginMetabaseProfileright::canProfileViewDashboard( - $_SESSION['glpiactiveprofile']['id'], - $dashboard['id'], - ); + $canView = self::canCurrentUserViewDashboard($dashboard['id']); return $isEmbeddingEnabled && $canView; }, diff --git a/inc/itemright.class.php b/inc/itemright.class.php new file mode 100644 index 0000000..72a882a --- /dev/null +++ b/inc/itemright.class.php @@ -0,0 +1,405 @@ +. + * ------------------------------------------------------------------------- + * @copyright Copyright (C) 2018-2023 by Metabase plugin team. + * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + * @link https://github.com/pluginsGLPI/metabase + * ------------------------------------------------------------------------- + * + * CUSTOM FORK NOTICE + * ------------------------------------------------------------------------- + * This class was added in a local fork to allow defining dashboard access + * rights per Group and per User, in addition to the native per Profile + * rights handled by PluginMetabaseProfileright. + * ------------------------------------------------------------------------- + */ + +class PluginMetabaseItemright extends CommonDBTM +{ + /** + * Itemtypes supported by this "generic" right holder. + */ + public const SUPPORTED_ITEMTYPES = [Group::class, User::class]; + + /** + * {@inheritDoc} + * @see CommonGLPI::getTypeName() + */ + public static function getTypeName($nb = 0) + { + return __s('Metabase', 'metabase'); + } + + /** + * Returns the GLPI right (and value) required to manage metabase + * dashboard rights for the given itemtype. + * + * @param string $itemtype Group::class or User::class + * + * @return array{0: string, 1: int} [rightname, right value] + */ + private static function getRequiredRight(string $itemtype): array + { + return match ($itemtype) { + Group::class => ['group', UPDATE], + User::class => ['user', UPDATE], + default => ['profile', UPDATE], + }; + } + + /** + * {@inheritDoc} + * @see CommonGLPI::getTabNameForItem() + */ + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) + { + $itemtype = $item::getType(); + + if (!in_array($itemtype, self::SUPPORTED_ITEMTYPES, true)) { + return ''; + } + + [$rightname, $rightvalue] = self::getRequiredRight($itemtype); + if (Session::haveRight($rightname, $rightvalue)) { + return self::createTabEntry(self::getTypeName(), 0, $itemtype, PluginMetabaseConfig::getIcon()); + } + + return ''; + } + + /** + * {@inheritDoc} + * @see CommonGLPI::displayTabContentForItem() + */ + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) + { + $itemtype = $item::getType(); + + if (!in_array($itemtype, self::SUPPORTED_ITEMTYPES, true)) { + return true; + } + + [$rightname, $rightvalue] = self::getRequiredRight($itemtype); + if (Session::haveRight($rightname, $rightvalue)) { + $itemright = new self(); + $itemright->showRightsForm($itemtype, $item->fields['id']); + } + + return true; + } + + /** + * Display item (group/user) rights form. + * + * @param string $itemtype Group::class or User::class + * @param integer $itemsId Group or User id + * @param array $options + * + * @return bool + */ + public function showRightsForm($itemtype, $itemsId, $options = []) + { + if (!in_array($itemtype, self::SUPPORTED_ITEMTYPES, true)) { + return false; + } + + [$rightname, $rightvalue] = self::getRequiredRight($itemtype); + if (!Session::haveRight($rightname, $rightvalue)) { + return false; + } + + $apiclient = new PluginMetabaseAPIClient(); + $dashboards = $apiclient->getDashboards(); + + if (!$dashboards) { + echo '
' . __s('No dashboards found in Metabase.', 'metabase') . '
'; + return false; + } + + echo '
'; + echo '
'; + echo ''; + + echo ''; + + Plugin::doHook('pre_item_form', ['item' => $this, 'options' => &$options]); + + echo ''; + + echo ''; + echo ''; + + echo ''; + echo ''; + echo ''; + + foreach ($dashboards as $dashboard) { + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo ''; + echo ''; + + echo '
' . self::getTypeName() . '
' . __s('Rights management', 'metabase') . '
'; + echo ''; + echo '   '; + echo ''; + echo '
' . $dashboard['name'] . ''; + Profile::dropdownRight( + sprintf('dashboard[%d]', $dashboard['id']), + [ + 'value' => self::getItemRightForDashboard($itemtype, $itemsId, $dashboard['id']), + 'nonone' => 0, + 'noread' => 0, + 'nowrite' => 1, + ], + ); + echo '
'; + echo Html::submit(_sx('button', 'Save'), [ + 'name' => 'update', + 'icon' => 'ti ti-device-floppy', + 'class' => 'btn btn-primary', + ]); + echo '
'; + echo '
'; + + Html::closeForm(); + + return true; + } + + /** + * Check if any group from the given list is able to view at least one dashboard. + * + * @param int[] $groupIds + * + * @return boolean + */ + public static function canGroupsViewDashboards(array $groupIds): bool + { + foreach ($groupIds as $groupId) { + if (self::canItemViewDashboards(Group::class, $groupId)) { + return true; + } + } + + return false; + } + + /** + * Check if any group from the given list is able to view the given dashboard. + * + * @param int[] $groupIds + * @param integer $dashboardUuid + * + * @return boolean + */ + public static function canGroupsViewDashboard(array $groupIds, $dashboardUuid): bool + { + foreach ($groupIds as $groupId) { + if (self::canItemViewDashboard(Group::class, $groupId, $dashboardUuid)) { + return true; + } + } + + return false; + } + + /** + * Check if item (group/user) is able to view at least one dashboard. + * + * @param string $itemtype + * @param integer $itemsId + * + * @return boolean + */ + public static function canItemViewDashboards($itemtype, $itemsId): bool + { + /** @var DBmysql $DB */ + global $DB; + + if (empty($itemsId)) { + return false; + } + + $iterator = $DB->request( + [ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'itemtype' => $itemtype, + 'items_id' => $itemsId, + ], + ], + ); + + foreach ($iterator as $right) { + if (($right['rights'] & READ) !== 0) { + return true; + } + } + + return false; + } + + /** + * Check if item (group/user) is able to view given dashboard. + * + * @param string $itemtype + * @param integer $itemsId + * @param integer $dashboardUuid + * + * @return boolean + */ + public static function canItemViewDashboard($itemtype, $itemsId, $dashboardUuid): bool + { + return (self::getItemRightForDashboard($itemtype, $itemsId, $dashboardUuid) & READ) !== 0; + } + + /** + * Returns item (group/user) rights for given dashboard. + * + * @param string $itemtype + * @param integer $itemsId + * @param integer $dashboardUuid + * + * @return integer + */ + private static function getItemRightForDashboard($itemtype, $itemsId, $dashboardUuid) + { + if (empty($itemsId)) { + return 0; + } + + $rightCriteria = [ + 'itemtype' => $itemtype, + 'items_id' => $itemsId, + 'dashboard_uuid' => $dashboardUuid, + ]; + + $itemright = new self(); + if ($itemright->getFromDBByCrit($rightCriteria)) { + return $itemright->fields['rights']; + } + + return 0; + } + + /** + * Defines item (group/user) rights for dashboard. + * + * @param string $itemtype + * @param integer $itemsId + * @param integer $dashboardUuid + * @param integer $rights + * + * @return void + */ + public static function setDashboardRightsForItem($itemtype, $itemsId, $dashboardUuid, $rights) + { + $itemright = new self(); + + $rightsExists = $itemright->getFromDBByCrit( + [ + 'itemtype' => $itemtype, + 'items_id' => $itemsId, + 'dashboard_uuid' => $dashboardUuid, + ], + ); + + if ($rightsExists) { + $itemright->update( + [ + 'id' => $itemright->fields['id'], + 'rights' => $rights, + ], + ); + } else { + $itemright->add( + [ + 'itemtype' => $itemtype, + 'items_id' => $itemsId, + 'dashboard_uuid' => $dashboardUuid, + 'rights' => $rights, + ], + ); + } + } + + /** + * Install itemrights database. + * + * @param Migration $migration + * + * @return void + */ + public static function install(Migration $migration) + { + /** @var DBmysql $DB */ + global $DB; + + $default_charset = DBConnection::getDefaultCharset(); + $default_collation = DBConnection::getDefaultCollation(); + $default_key_sign = DBConnection::getDefaultPrimaryKeySignOption(); + + $table = self::getTable(); + + if (!$DB->tableExists($table)) { + $migration->displayMessage("Installing $table"); + + $query = "CREATE TABLE IF NOT EXISTS `$table` ( + `id` int {$default_key_sign} NOT NULL AUTO_INCREMENT, + `itemtype` varchar(100) NOT NULL, + `items_id` int {$default_key_sign} NOT NULL, + `dashboard_uuid` int NOT NULL, + `rights` int NOT NULL, + PRIMARY KEY (`id`), + UNIQUE `itemtype_items_id_dashboard_uuid` (`itemtype`, `items_id`, `dashboard_uuid`) + ) ENGINE=InnoDB DEFAULT CHARSET={$default_charset} COLLATE={$default_collation} ROW_FORMAT=DYNAMIC;"; + $DB->doQuery($query); + } + } + + /** + * Uninstall itemrights database. + * + * @return void + */ + public static function uninstall() + { + /** @var DBmysql $DB */ + global $DB; + + $DB->doQuery('DROP TABLE IF EXISTS `' . self::getTable() . '`'); + } +} diff --git a/setup.php b/setup.php index ef8d9e9..320ec0b 100644 --- a/setup.php +++ b/setup.php @@ -78,7 +78,7 @@ function plugin_init_metabase() //display helpdesk menu if self-service and if is able to view at least one dashboard. if ( $_SESSION['glpiactiveprofile']['interface'] == 'helpdesk' - && PluginMetabaseProfileright::canProfileViewDashboards($_SESSION['glpiactiveprofile']['id']) + && PluginMetabaseDashboard::canCurrentUserViewDashboards() ) { $PLUGIN_HOOKS['helpdesk_menu_entry']['metabase'] = '/front/selfservice.php'; $PLUGIN_HOOKS['helpdesk_menu_entry_icon']['metabase'] = 'ti ti-chart-bar'; @@ -88,6 +88,9 @@ function plugin_init_metabase() // profile rights management Plugin::registerClass('PluginMetabaseProfileright', ['addtabon' => 'Profile']); + // CUSTOM FORK: group / user rights management + Plugin::registerClass('PluginMetabaseItemright', ['addtabon' => ['Group', 'User']]); + // css & js $PLUGIN_HOOKS['add_css']['metabase'] = 'metabase.css'; $PLUGIN_HOOKS['add_javascript']['metabase'] = 'metabase.js'; From 0e5679e383768b56bc4723434885c8486bf8f2ad Mon Sep 17 00:00:00 2001 From: Michael Rodrigues <143286293+MichaelRodriguesOficial@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:42:42 -0300 Subject: [PATCH 2/6] Update inc/dashboard.class.php Co-authored-by: Stanislas --- inc/dashboard.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/dashboard.class.php b/inc/dashboard.class.php index d9fca74..2336338 100644 --- a/inc/dashboard.class.php +++ b/inc/dashboard.class.php @@ -72,7 +72,7 @@ public static function canCurrentUserViewDashboards(): bool * Check if the currently logged-in user is able to view the given * dashboard, combining profile, group and user based rights. * - * CUSTOM FORK: rights are additive (OR) across profile / groups / user. + * rights are additive (OR) across profile / groups / user. * * @param integer $dashboardUuid * From 2adcd3400bfa9ff751b0e0b5f01c2adb518694c8 Mon Sep 17 00:00:00 2001 From: Michael Rodrigues <143286293+MichaelRodriguesOficial@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:00:53 -0300 Subject: [PATCH 3/6] Update inc/dashboard.class.php Co-authored-by: Stanislas --- inc/dashboard.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/dashboard.class.php b/inc/dashboard.class.php index 2336338..5995f17 100644 --- a/inc/dashboard.class.php +++ b/inc/dashboard.class.php @@ -78,7 +78,7 @@ public static function canCurrentUserViewDashboards(): bool * * @return boolean */ - public static function canCurrentUserViewDashboard($dashboardUuid): bool + public static function canCurrentUserViewDashboard(int $dashboardUuid): bool { if (PluginMetabaseProfileright::canProfileViewDashboard($_SESSION['glpiactiveprofile']['id'], $dashboardUuid)) { return true; From 6c833b6d3ef62bc93e4745c2a2437f8661d003fb Mon Sep 17 00:00:00 2001 From: Michael Rodrigues <143286293+MichaelRodriguesOficial@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:20:50 -0300 Subject: [PATCH 4/6] Update inc/itemright.class.php Co-authored-by: Stanislas --- inc/itemright.class.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/inc/itemright.class.php b/inc/itemright.class.php index 72a882a..813cb0c 100644 --- a/inc/itemright.class.php +++ b/inc/itemright.class.php @@ -227,8 +227,24 @@ public static function canGroupsViewDashboards(array $groupIds): bool */ public static function canGroupsViewDashboard(array $groupIds, $dashboardUuid): bool { - foreach ($groupIds as $groupId) { - if (self::canItemViewDashboard(Group::class, $groupId, $dashboardUuid)) { + if (empty($groupIds)) { + return false; + } + + /** @var DBmysql $DB */ + global $DB; + + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'itemtype' => Group::class, + 'items_id' => $groupIds, + 'dashboard_uuid' => $dashboardUuid, + ], + ]); + + foreach ($iterator as $right) { + if (($right['rights'] & READ) !== 0) { return true; } } From f41c70298c20c10a1f9d57f19a67d36d5ad7d0f8 Mon Sep 17 00:00:00 2001 From: Michael Rodrigues <143286293+MichaelRodriguesOficial@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:53:37 -0300 Subject: [PATCH 5/6] Update inc/itemright.class.php Co-authored-by: Romain B. <8530352+Rom1-B@users.noreply.github.com> --- inc/itemright.class.php | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/inc/itemright.class.php b/inc/itemright.class.php index 813cb0c..ec3d43c 100644 --- a/inc/itemright.class.php +++ b/inc/itemright.class.php @@ -222,9 +222,32 @@ public static function canGroupsViewDashboards(array $groupIds): bool * * @param int[] $groupIds * @param integer $dashboardUuid - * - * @return boolean - */ + public static function canGroupsViewDashboards(array $groupIds): bool + { + if (empty($groupIds)) { + return false; + } + + /** @var DBmysql $DB */ + global $DB; + + $iterator = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'itemtype' => Group::class, + 'items_id' => $groupIds, + ], + ]); + + foreach ($iterator as $right) { + if (($right['rights'] & READ) !== 0) { + return true; + } + } + + return false; + } + public static function canGroupsViewDashboard(array $groupIds, $dashboardUuid): bool { if (empty($groupIds)) { From a8473fa2d506cbe21714ba98eabbfd0f48b41173 Mon Sep 17 00:00:00 2001 From: Michael Rodrigues Date: Fri, 16 Oct 2026 14:36:21 -0300 Subject: [PATCH 6/6] Rework group/user dashboard rights to address review - Fix PHP syntax error introduced by the docblock suggestion in itemright.class.php - Render the rights matrix with a Twig template (no more echo) - READ on the group/user is enough to see the tab, UPDATE is required to save - Check rights on the targeted group/user itself, not only the global right - Rename dashboard_uuid column to dashboard_id - Add int type hint and docblock on canCurrentUserViewDashboard() - Remove rights when a group or user is purged - Rebase on GLPI 12 / 1.5.0 and add CHANGELOG entry Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0115rGXdmY7mffoJzXeFPGhw --- CHANGELOG.md | 6 + front/itemright.form.php | 112 ++++------ inc/dashboard.class.php | 52 ++--- inc/itemright.class.php | 390 ++++++++++++---------------------- setup.php | 8 +- templates/itemright.html.twig | 76 +++++++ 6 files changed, 280 insertions(+), 364 deletions(-) create mode 100644 templates/itemright.html.twig diff --git a/CHANGELOG.md b/CHANGELOG.md index 9901b8b..495f05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Added + +- Manage dashboard access rights per Group and per User (in addition to per Profile) + ## [1.5.0] - 2026-10-06 ### Added diff --git a/front/itemright.form.php b/front/itemright.form.php index ede1891..426ed64 100644 --- a/front/itemright.form.php +++ b/front/itemright.form.php @@ -26,38 +26,33 @@ * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html * @link https://github.com/pluginsGLPI/metabase * ------------------------------------------------------------------------- - * - * CUSTOM FORK NOTICE: added to handle per Group / per User dashboard rights. - * ------------------------------------------------------------------------- */ -function plugin_metabase_check_itemright_access($itemtype) -{ - if (!in_array($itemtype, PluginMetabaseItemright::SUPPORTED_ITEMTYPES, true)) { - Session::addMessageAfterRedirect( - __s('Invalid request.', 'metabase'), - false, - ERROR, - ); - Html::back(); - } +$itemtype = $_POST['itemtype'] ?? null; +$items_id = (int) ($_POST['items_id'] ?? 0); - if ($itemtype === Group::class) { - Session::checkRight('group', UPDATE); - } else { - Session::checkRight('user', UPDATE); - } +$item = match ($itemtype) { + Group::class => new Group(), + User::class => new User(), + default => null, +}; + +if ($item === null || $items_id <= 0 || !$item->getFromDB($items_id)) { + Session::addMessageAfterRedirect( + __s('Invalid request.', 'metabase'), + false, + ERROR, + ); + Html::back(); } -if (isset($_REQUEST['update'])) { - if ( - !array_key_exists('itemtype', $_REQUEST) - || empty($_REQUEST['itemtype']) - || !array_key_exists('items_id', $_REQUEST) - || empty($_REQUEST['items_id']) - || !array_key_exists('dashboard', $_REQUEST) - || !is_array($_REQUEST['dashboard']) - ) { +// UPDATE right on the targeted group / user is required to change its dashboard rights. +$item->check($items_id, UPDATE); + +$viewable_dashboards_ids = []; + +if (isset($_POST['update'])) { + if (!isset($_POST['dashboard']) || !is_array($_POST['dashboard'])) { Session::addMessageAfterRedirect( __s('Invalid request.', 'metabase'), false, @@ -66,60 +61,33 @@ function plugin_metabase_check_itemright_access($itemtype) Html::back(); } - plugin_metabase_check_itemright_access($_REQUEST['itemtype']); + foreach ($_POST['dashboard'] as $dashboard_id => $rights) { + $rights = ((int) $rights & READ) !== 0 ? READ : 0; - $viewableDashboardsUuids = []; - foreach ($_REQUEST['dashboard'] as $dashboardUuid => $rights) { - PluginMetabaseItemright::setDashboardRightsForItem( - $_REQUEST['itemtype'], - $_REQUEST['items_id'], - $dashboardUuid, - $rights, - ); + PluginMetabaseItemright::setDashboardRightsForItem($item::class, $items_id, (int) $dashboard_id, $rights); - if (($rights & READ) !== 0) { - $viewableDashboardsUuids[] = $dashboardUuid; + if ($rights !== 0) { + $viewable_dashboards_ids[] = (int) $dashboard_id; } } +} elseif (isset($_POST['set_rights_to_all'])) { + $rights = ((int) $_POST['set_rights_to_all'] & READ) !== 0 ? READ : 0; - if (!empty($viewableDashboardsUuids)) { - $apiclient = new PluginMetabaseAPIClient(); - $apiclient->enableDashboardsEmbeddedDisplay($viewableDashboardsUuids); - } -} elseif (isset($_REQUEST['set_rights_to_all'])) { - if ( - !array_key_exists('itemtype', $_REQUEST) - || empty($_REQUEST['itemtype']) - || !array_key_exists('items_id', $_REQUEST) - || empty($_REQUEST['items_id']) - ) { - Session::addMessageAfterRedirect( - __s('Invalid request.', 'metabase'), - false, - ERROR, - ); - Html::back(); - } - - plugin_metabase_check_itemright_access($_REQUEST['itemtype']); + $apiclient = new PluginMetabaseAPIClient(); + $dashboards = $apiclient->getDashboards(); - $apiclient = new PluginMetabaseAPIClient(); - - $viewableDashboardsUuids = []; - foreach ($apiclient->getDashboards() as $dashboard) { - PluginMetabaseItemright::setDashboardRightsForItem( - $_REQUEST['itemtype'], - $_REQUEST['items_id'], - $dashboard['id'], - $_REQUEST['set_rights_to_all'], - ); + foreach (is_array($dashboards) ? $dashboards : [] as $dashboard) { + PluginMetabaseItemright::setDashboardRightsForItem($item::class, $items_id, (int) $dashboard['id'], $rights); - $viewableDashboardsUuids[] = $dashboard['id']; + if ($rights !== 0) { + $viewable_dashboards_ids[] = (int) $dashboard['id']; + } } +} - if ((int) $_REQUEST['set_rights_to_all'] !== 0) { - $apiclient->enableDashboardsEmbeddedDisplay($viewableDashboardsUuids); - } +if ($viewable_dashboards_ids !== []) { + $apiclient = new PluginMetabaseAPIClient(); + $apiclient->enableDashboardsEmbeddedDisplay($viewable_dashboards_ids); } Html::back(); diff --git a/inc/dashboard.class.php b/inc/dashboard.class.php index 683916e..5b9a0ff 100644 --- a/inc/dashboard.class.php +++ b/inc/dashboard.class.php @@ -44,55 +44,33 @@ public static function getTypeName($nb = 0) } /** - * Check if the currently logged-in user is able to view at least one - * dashboard, combining profile, group and user based rights. + * Check if the current user is able to view at least one dashboard. * - * CUSTOM FORK: rights are additive (OR) across profile / groups / user. + * Rights are additive (OR) across profile / groups / user. * - * @return boolean + * @return bool */ public static function canCurrentUserViewDashboards(): bool { - if (PluginMetabaseProfileright::canProfileViewDashboards($_SESSION['glpiactiveprofile']['id'])) { - return true; - } - - if (PluginMetabaseItemright::canItemViewDashboards(User::class, Session::getLoginUserID())) { - return true; - } - - if (PluginMetabaseItemright::canGroupsViewDashboards($_SESSION['glpigroups'] ?? [])) { - return true; - } - - return false; + return PluginMetabaseProfileright::canProfileViewDashboards($_SESSION['glpiactiveprofile']['id']) + || PluginMetabaseItemright::canItemsViewDashboards(User::class, [Session::getLoginUserID()]) + || PluginMetabaseItemright::canItemsViewDashboards(Group::class, $_SESSION['glpigroups'] ?? []); } /** - * Check if the currently logged-in user is able to view the given - * dashboard, combining profile, group and user based rights. + * Check if the current user is able to view the given dashboard. * - * rights are additive (OR) across profile / groups / user. + * Rights are additive (OR) across profile / groups / user. * - * @param integer $dashboardUuid + * @param int $dashboardId Metabase dashboard id * - * @return boolean + * @return bool */ - public static function canCurrentUserViewDashboard(int $dashboardUuid): bool + public static function canCurrentUserViewDashboard(int $dashboardId): bool { - if (PluginMetabaseProfileright::canProfileViewDashboard($_SESSION['glpiactiveprofile']['id'], $dashboardUuid)) { - return true; - } - - if (PluginMetabaseItemright::canItemViewDashboard(User::class, Session::getLoginUserID(), $dashboardUuid)) { - return true; - } - - if (PluginMetabaseItemright::canGroupsViewDashboard($_SESSION['glpigroups'] ?? [], $dashboardUuid)) { - return true; - } - - return false; + return PluginMetabaseProfileright::canProfileViewDashboard($_SESSION['glpiactiveprofile']['id'], $dashboardId) !== 0 + || PluginMetabaseItemright::canItemsViewDashboard(User::class, [Session::getLoginUserID()], $dashboardId) + || PluginMetabaseItemright::canItemsViewDashboard(Group::class, $_SESSION['glpigroups'] ?? [], $dashboardId); } /** @@ -151,7 +129,7 @@ public static function showForCentral(Central $item, $withtemplate = 0, $is_help $dashboards, function ($dashboard) { $isEmbeddingEnabled = $dashboard['enable_embedding']; - $canView = self::canCurrentUserViewDashboard($dashboard['id']); + $canView = self::canCurrentUserViewDashboard((int) $dashboard['id']); return $isEmbeddingEnabled && $canView; }, diff --git a/inc/itemright.class.php b/inc/itemright.class.php index ec3d43c..91f7df3 100644 --- a/inc/itemright.class.php +++ b/inc/itemright.class.php @@ -26,19 +26,20 @@ * @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html * @link https://github.com/pluginsGLPI/metabase * ------------------------------------------------------------------------- - * - * CUSTOM FORK NOTICE - * ------------------------------------------------------------------------- - * This class was added in a local fork to allow defining dashboard access - * rights per Group and per User, in addition to the native per Profile - * rights handled by PluginMetabaseProfileright. - * ------------------------------------------------------------------------- */ +use Glpi\Application\View\TemplateRenderer; + +/** + * Dashboard access rights granted to a Group or to a User. + * + * Works alongside PluginMetabaseProfileright: a dashboard is visible when the + * active profile, one of the user's groups or the user itself has READ access. + */ class PluginMetabaseItemright extends CommonDBTM { /** - * Itemtypes supported by this "generic" right holder. + * Itemtypes that can receive dashboard rights. */ public const SUPPORTED_ITEMTYPES = [Group::class, User::class]; @@ -52,20 +53,15 @@ public static function getTypeName($nb = 0) } /** - * Returns the GLPI right (and value) required to manage metabase - * dashboard rights for the given itemtype. + * Check whether given itemtype can receive dashboard rights. * - * @param string $itemtype Group::class or User::class + * @param mixed $itemtype * - * @return array{0: string, 1: int} [rightname, right value] + * @return bool */ - private static function getRequiredRight(string $itemtype): array + public static function isSupportedItemtype($itemtype): bool { - return match ($itemtype) { - Group::class => ['group', UPDATE], - User::class => ['user', UPDATE], - default => ['profile', UPDATE], - }; + return in_array($itemtype, self::SUPPORTED_ITEMTYPES, true); } /** @@ -74,15 +70,13 @@ private static function getRequiredRight(string $itemtype): array */ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { - $itemtype = $item::getType(); - - if (!in_array($itemtype, self::SUPPORTED_ITEMTYPES, true)) { - return ''; - } - - [$rightname, $rightvalue] = self::getRequiredRight($itemtype); - if (Session::haveRight($rightname, $rightvalue)) { - return self::createTabEntry(self::getTypeName(), 0, $itemtype, PluginMetabaseConfig::getIcon()); + if ( + $item instanceof CommonDBTM + && self::isSupportedItemtype($item::class) + && !$item->isNewItem() + && $item::canView() + ) { + return self::createTabEntry(self::getTypeName(), 0, $item::getType(), PluginMetabaseConfig::getIcon()); } return ''; @@ -94,309 +88,198 @@ public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) */ public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { - $itemtype = $item::getType(); - - if (!in_array($itemtype, self::SUPPORTED_ITEMTYPES, true)) { - return true; - } - - [$rightname, $rightvalue] = self::getRequiredRight($itemtype); - if (Session::haveRight($rightname, $rightvalue)) { - $itemright = new self(); - $itemright->showRightsForm($itemtype, $item->fields['id']); + if ( + $item instanceof CommonDBTM + && self::isSupportedItemtype($item::class) + && $item->can($item->getID(), READ) + ) { + self::showForItem($item); } return true; } /** - * Display item (group/user) rights form. + * Display dashboard rights matrix for given group or user. + * + * READ right on the item is required to see the matrix, + * UPDATE right on the item is required to change it. * - * @param string $itemtype Group::class or User::class - * @param integer $itemsId Group or User id - * @param array $options + * @param CommonDBTM $item Group or User * * @return bool */ - public function showRightsForm($itemtype, $itemsId, $options = []) + public static function showForItem(CommonDBTM $item): bool { - if (!in_array($itemtype, self::SUPPORTED_ITEMTYPES, true)) { - return false; - } - - [$rightname, $rightvalue] = self::getRequiredRight($itemtype); - if (!Session::haveRight($rightname, $rightvalue)) { - return false; - } - $apiclient = new PluginMetabaseAPIClient(); $dashboards = $apiclient->getDashboards(); - if (!$dashboards) { - echo '
' . __s('No dashboards found in Metabase.', 'metabase') . '
'; + if (!is_array($dashboards) || $dashboards === []) { + TemplateRenderer::getInstance()->display('@metabase/itemright.html.twig', [ + 'no_dashboard' => true, + ]); return false; } - echo ''; - echo '
'; - echo ''; - - echo ''; - - Plugin::doHook('pre_item_form', ['item' => $this, 'options' => &$options]); - - echo ''; - - echo ''; - echo ''; - - echo ''; - echo ''; - echo ''; + $rights = self::getRightsForItem($item::class, $item->getID()); + $dashboards_data = []; foreach ($dashboards as $dashboard) { - echo ''; - echo ''; - echo ''; - echo ''; + $dashboards_data[] = [ + 'id' => (int) $dashboard['id'], + 'name' => $dashboard['name'], + 'rights' => $rights[(int) $dashboard['id']] ?? 0, + ]; } - echo ''; - echo ''; - echo ''; - - echo '
' . self::getTypeName() . '
' . __s('Rights management', 'metabase') . '
'; - echo ''; - echo '   '; - echo ''; - echo '
' . $dashboard['name'] . ''; - Profile::dropdownRight( - sprintf('dashboard[%d]', $dashboard['id']), - [ - 'value' => self::getItemRightForDashboard($itemtype, $itemsId, $dashboard['id']), - 'nonone' => 0, - 'noread' => 0, - 'nowrite' => 1, - ], - ); - echo '
'; - echo Html::submit(_sx('button', 'Save'), [ - 'name' => 'update', - 'icon' => 'ti ti-device-floppy', - 'class' => 'btn btn-primary', + TemplateRenderer::getInstance()->display('@metabase/itemright.html.twig', [ + 'no_dashboard' => false, + 'form_url' => self::getFormURL(), + 'itemtype' => $item::class, + 'items_id' => $item->getID(), + 'can_update' => $item->can($item->getID(), UPDATE), + 'dashboards' => $dashboards_data, + 'right_choices' => [ + 0 => __('No access'), + READ => __('Read'), + ], ]); - echo '
'; - echo '
'; - - Html::closeForm(); return true; } /** - * Check if any group from the given list is able to view at least one dashboard. + * Check if the given groups or user are able to view at least one dashboard. * - * @param int[] $groupIds + * @param string $itemtype Group::class or User::class + * @param int[] $items_ids * - * @return boolean + * @return bool */ - public static function canGroupsViewDashboards(array $groupIds): bool + public static function canItemsViewDashboards(string $itemtype, array $items_ids): bool { - foreach ($groupIds as $groupId) { - if (self::canItemViewDashboards(Group::class, $groupId)) { - return true; - } - } - - return false; + return self::countViewableRights($itemtype, $items_ids) > 0; } /** - * Check if any group from the given list is able to view the given dashboard. + * Check if the given groups or user are able to view given dashboard. + * + * @param string $itemtype Group::class or User::class + * @param int[] $items_ids + * @param int $dashboard_id Metabase dashboard id * - * @param int[] $groupIds - * @param integer $dashboardUuid - public static function canGroupsViewDashboards(array $groupIds): bool + * @return bool + */ + public static function canItemsViewDashboard(string $itemtype, array $items_ids, int $dashboard_id): bool { - if (empty($groupIds)) { - return false; - } - - /** @var DBmysql $DB */ - global $DB; - - $iterator = $DB->request([ - 'FROM' => self::getTable(), - 'WHERE' => [ - 'itemtype' => Group::class, - 'items_id' => $groupIds, - ], - ]); - - foreach ($iterator as $right) { - if (($right['rights'] & READ) !== 0) { - return true; - } - } - - return false; + return self::countViewableRights($itemtype, $items_ids, $dashboard_id) > 0; } - public static function canGroupsViewDashboard(array $groupIds, $dashboardUuid): bool + /** + * Count rights rows granting READ access to the given items. + * + * @param string $itemtype + * @param int[] $items_ids + * @param int|null $dashboard_id Restrict to this dashboard if not null + * + * @return int + */ + private static function countViewableRights(string $itemtype, array $items_ids, ?int $dashboard_id = null): int { - if (empty($groupIds)) { - return false; + $items_ids = array_values(array_filter(array_map('intval', $items_ids))); + if (!self::isSupportedItemtype($itemtype) || $items_ids === []) { + return 0; } - /** @var DBmysql $DB */ - global $DB; - - $iterator = $DB->request([ - 'FROM' => self::getTable(), - 'WHERE' => [ - 'itemtype' => Group::class, - 'items_id' => $groupIds, - 'dashboard_uuid' => $dashboardUuid, - ], - ]); - - foreach ($iterator as $right) { - if (($right['rights'] & READ) !== 0) { - return true; - } + $where = [ + 'itemtype' => $itemtype, + 'items_id' => $items_ids, + 'rights' => ['&', READ], + ]; + if ($dashboard_id !== null) { + $where['dashboard_id'] = $dashboard_id; } - return false; + return countElementsInTable(self::getTable(), $where); } /** - * Check if item (group/user) is able to view at least one dashboard. + * Returns rights of given item, indexed by dashboard id. * - * @param string $itemtype - * @param integer $itemsId + * @param string $itemtype + * @param int $items_id * - * @return boolean + * @return array */ - public static function canItemViewDashboards($itemtype, $itemsId): bool + private static function getRightsForItem(string $itemtype, int $items_id): array { /** @var DBmysql $DB */ global $DB; - if (empty($itemsId)) { - return false; - } - - $iterator = $DB->request( - [ - 'FROM' => self::getTable(), - 'WHERE' => [ - 'itemtype' => $itemtype, - 'items_id' => $itemsId, - ], + $iterator = $DB->request([ + 'SELECT' => ['dashboard_id', 'rights'], + 'FROM' => self::getTable(), + 'WHERE' => [ + 'itemtype' => $itemtype, + 'items_id' => $items_id, ], - ); + ]); - foreach ($iterator as $right) { - if (($right['rights'] & READ) !== 0) { - return true; - } + $rights = []; + foreach ($iterator as $row) { + $rights[(int) $row['dashboard_id']] = (int) $row['rights']; } - return false; + return $rights; } /** - * Check if item (group/user) is able to view given dashboard. + * Defines rights of given item for given dashboard. * - * @param string $itemtype - * @param integer $itemsId - * @param integer $dashboardUuid + * @param string $itemtype Group::class or User::class + * @param int $items_id + * @param int $dashboard_id Metabase dashboard id + * @param int $rights * - * @return boolean - */ - public static function canItemViewDashboard($itemtype, $itemsId, $dashboardUuid): bool - { - return (self::getItemRightForDashboard($itemtype, $itemsId, $dashboardUuid) & READ) !== 0; - } - - /** - * Returns item (group/user) rights for given dashboard. - * - * @param string $itemtype - * @param integer $itemsId - * @param integer $dashboardUuid - * - * @return integer + * @return void */ - private static function getItemRightForDashboard($itemtype, $itemsId, $dashboardUuid) + public static function setDashboardRightsForItem(string $itemtype, int $items_id, int $dashboard_id, int $rights): void { - if (empty($itemsId)) { - return 0; - } - - $rightCriteria = [ - 'itemtype' => $itemtype, - 'items_id' => $itemsId, - 'dashboard_uuid' => $dashboardUuid, + $criteria = [ + 'itemtype' => $itemtype, + 'items_id' => $items_id, + 'dashboard_id' => $dashboard_id, ]; $itemright = new self(); - if ($itemright->getFromDBByCrit($rightCriteria)) { - return $itemright->fields['rights']; + if ($itemright->getFromDBByCrit($criteria)) { + $itemright->update([ + 'id' => $itemright->getID(), + 'rights' => $rights, + ]); + } else { + $itemright->add($criteria + ['rights' => $rights]); } - - return 0; } /** - * Defines item (group/user) rights for dashboard. + * Remove rights of a purged group or user. * - * @param string $itemtype - * @param integer $itemsId - * @param integer $dashboardUuid - * @param integer $rights + * @param CommonDBTM $item * * @return void */ - public static function setDashboardRightsForItem($itemtype, $itemsId, $dashboardUuid, $rights) + public static function cleanForItem(CommonDBTM $item): void { $itemright = new self(); - - $rightsExists = $itemright->getFromDBByCrit( - [ - 'itemtype' => $itemtype, - 'items_id' => $itemsId, - 'dashboard_uuid' => $dashboardUuid, - ], - ); - - if ($rightsExists) { - $itemright->update( - [ - 'id' => $itemright->fields['id'], - 'rights' => $rights, - ], - ); - } else { - $itemright->add( - [ - 'itemtype' => $itemtype, - 'items_id' => $itemsId, - 'dashboard_uuid' => $dashboardUuid, - 'rights' => $rights, - ], - ); - } + $itemright->deleteByCriteria([ + 'itemtype' => $item::class, + 'items_id' => $item->getID(), + ]); } /** - * Install itemrights database. + * Install item rights database. * * @param Migration $migration * @@ -420,17 +303,18 @@ public static function install(Migration $migration) `id` int {$default_key_sign} NOT NULL AUTO_INCREMENT, `itemtype` varchar(100) NOT NULL, `items_id` int {$default_key_sign} NOT NULL, - `dashboard_uuid` int NOT NULL, + `dashboard_id` int NOT NULL, `rights` int NOT NULL, PRIMARY KEY (`id`), - UNIQUE `itemtype_items_id_dashboard_uuid` (`itemtype`, `items_id`, `dashboard_uuid`) + UNIQUE `unicity` (`itemtype`, `items_id`, `dashboard_id`), + KEY `dashboard_id` (`dashboard_id`) ) ENGINE=InnoDB DEFAULT CHARSET={$default_charset} COLLATE={$default_collation} ROW_FORMAT=DYNAMIC;"; $DB->doQuery($query); } } /** - * Uninstall itemrights database. + * Uninstall item rights database. * * @return void */ diff --git a/setup.php b/setup.php index 0b49077..4668bd1 100644 --- a/setup.php +++ b/setup.php @@ -88,8 +88,12 @@ function plugin_init_metabase() // profile rights management Plugin::registerClass('PluginMetabaseProfileright', ['addtabon' => 'Profile']); - // CUSTOM FORK: group / user rights management - Plugin::registerClass('PluginMetabaseItemright', ['addtabon' => ['Group', 'User']]); + // group and user rights management + Plugin::registerClass('PluginMetabaseItemright', ['addtabon' => [Group::class, User::class]]); + $PLUGIN_HOOKS['item_purge']['metabase'] = [ + Group::class => [PluginMetabaseItemright::class, 'cleanForItem'], + User::class => [PluginMetabaseItemright::class, 'cleanForItem'], + ]; // css & js $PLUGIN_HOOKS['add_css']['metabase'] = 'metabase.css'; diff --git a/templates/itemright.html.twig b/templates/itemright.html.twig new file mode 100644 index 0000000..3bdc5b4 --- /dev/null +++ b/templates/itemright.html.twig @@ -0,0 +1,76 @@ +{# + # ------------------------------------------------------------------------- + # Metabase plugin for GLPI + # ------------------------------------------------------------------------- + # + # LICENSE + # + # This file is part of Metabase. + # + # Metabase is free software; you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation; either version 2 of the License, or + # (at your option) any later version. + # + # Metabase is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + # GNU General Public License for more details. + # + # You should have received a copy of the GNU General Public License + # along with Metabase. If not, see . + # ------------------------------------------------------------------------- + # @copyright Copyright (C) 2018-2023 by Metabase plugin team. + # @license GPLv2 https://www.gnu.org/licenses/gpl-2.0.html + # @link https://github.com/pluginsGLPI/metabase + # ------------------------------------------------------------------------- + #} + +{% import 'components/form/fields_macros.html.twig' as fields %} + +{% if no_dashboard %} +
+ {{ __('No dashboards found in Metabase.', 'metabase') }} +
+{% else %} + + {{ fields.hiddenField('itemtype', itemtype) }} + {{ fields.hiddenField('items_id', items_id) }} + + {{ fields.largeTitle(__('Rights management', 'metabase'), 'ti ti-lock') }} + + {% if can_update %} +
+ + +
+ {% endif %} + + {% for dashboard in dashboards %} + {{ fields.dropdownArrayField( + 'dashboard[' ~ dashboard.id ~ ']', + dashboard.rights, + right_choices, + dashboard.name, + { + disabled: not can_update, + } + ) }} + {% endfor %} + + {% if can_update %} +
+ +
+ {% endif %} + +{% endif %}