Skip to content

Manage dashboard access rights per Group and per User - #144

Open
MichaelRodriguesOficial wants to merge 3 commits into
pluginsGLPI:mainfrom
MichaelRodriguesOficial:main
Open

Manage dashboard access rights per Group and per User#144
MichaelRodriguesOficial wants to merge 3 commits into
pluginsGLPI:mainfrom
MichaelRodriguesOficial:main

Conversation

@MichaelRodriguesOficial

Copy link
Copy Markdown

Update to better manage access to the dashboards.

Checklist before requesting a review

  • I have performed a self-review of my code.
  • I have added tests (when available) that prove my fix is effective or that my feature works.
  • I have updated the CHANGELOG with a short functional description of the fix or new feature.
  • This change requires a documentation update.

Description

Currently, dashboard access can only be granted per Profile. This PR adds the ability to also grant (or deny) access per Group and per User, using the same rights mechanism (Profile::dropdownRight) already used for profiles.

  • Adds a new PluginMetabaseItemright class, mirroring PluginMetabaseProfileright, storing rights keyed by itemtype (Group or User) + items_id + dashboard_uuid.
  • Adds a new tab ("Metabase") on the Group and User forms, gated by the existing group/user UPDATE rights, to manage dashboard access for that group/user.
  • Access rights are now additive: a dashboard is shown on Central if the user's profile, any of their groups, or the user themselves has been granted READ access.
  • No changes to the existing profile-based rights: they keep working exactly as before, this is purely additive.

I did not add automated tests since the plugin doesn't currently have a test suite in place.

Screenshots:

Group User

Update to better manage access to the dashboards.
@stonebuzz
stonebuzz requested review from Rom1-B and stonebuzz July 17, 2026 07:28
@stonebuzz stonebuzz assigned stonebuzz and unassigned stonebuzz Jul 17, 2026
@stonebuzz stonebuzz added the enhancement New feature or request label Jul 17, 2026
Comment thread inc/itemright.class.php
*
* @return bool
*/
public function showRightsForm($itemtype, $itemsId, $options = [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use Twig templates instead.

Direct echo statements in PHP are no longer allowed. All output should be rendered through Twig templates to comply with the current coding standards.

Comment thread inc/itemright.class.php
Comment on lines +228 to +237
public static function canGroupsViewDashboard(array $groupIds, $dashboardUuid): bool
{
foreach ($groupIds as $groupId) {
if (self::canItemViewDashboard(Group::class, $groupId, $dashboardUuid)) {
return true;
}
}

return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canGroupsViewDashboard() makes one DB query per group (via getItemRightForDashboard). It is called from canCurrentUserViewDashboard(), which is itself called inside the array_filter loop in showForCentral() — once per dashboard. With D dashboards and G groups this produces D×G queries on every Central page load.

Replace with a single IN query on items_id — fetches all matching group rows at once and filters & READ in PHP. Apply the same pattern to canGroupsViewDashboards().

Suggested change
public static function canGroupsViewDashboard(array $groupIds, $dashboardUuid): bool
{
foreach ($groupIds as $groupId) {
if (self::canItemViewDashboard(Group::class, $groupId, $dashboardUuid)) {
return true;
}
}
return false;
}
public static function canGroupsViewDashboard(array $groupIds, $dashboardUuid): bool
{
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;
}
}
return false;
}

Comment thread inc/dashboard.class.php Outdated
Comment thread inc/dashboard.class.php Outdated
Comment thread inc/itemright.class.php
`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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be dashboard_id ?

Comment thread inc/itemright.class.php
Comment on lines +209 to +237
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canGroupsViewDashboard() (line 228) issues one DB query per group via getItemRightForDashboard(), and it's called once per dashboard from the array_filter loop in showForCentral() — D dashboards × G groups = D×G queries on every Central page load. stonebuzz's suggestion replaces it with a single IN-query on items_id, filtering & READ in PHP, and asks for the same pattern in canGroupsViewDashboards() (line 209), which has the identical per-group query loop.

Beyond the group path: canItemViewDashboard(User::class, ...) is also called once per dashboard from the same array_filter (via canCurrentUserViewDashboard()), adding D more single-row queries. Fixing only the group path still leaves D user queries per page load. Worth batching the User::class and Group::class checks in the same pass once at the top of showForCentral() rather than per-dashboard.

Suggested change
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;
}
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)) {
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;
}
}
return false;
}

Comment thread inc/itemright.class.php
* {@inheritDoc}
* @see CommonGLPI::getTabNameForItem()
*/
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTabNameForItem(), displayTabContentForItem() and showRightsForm() are all gated behind the UPDATE right only (line 84, 104, 128). PluginMetabaseProfileright (the class this PR explicitly mirrors) gates its tab/form behind READ and only wraps the "Allow/Disallow all" buttons in an extra UPDATE check (inc/profileright.class.php:56,109), so a user with read-only rights on Profile can still see the profile-dashboard matrix. Here, a user with only group/user READ cannot see the tab at all. If this asymmetry is intentional (e.g. group/user READ being too broadly granted to expose dashboard rights read-only), it's worth a one-line comment; otherwise consider splitting the gate like the profile form does.

MichaelRodriguesOficial and others added 2 commits July 23, 2026 17:42
Co-authored-by: Stanislas <skita@teclib.com>
Co-authored-by: Stanislas <skita@teclib.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants