Skip to content

Bug: AJAX tooltip for project tasks always shows "Access Forbidden" for restricted users #37453

@avadnc

Description

@avadnc

Bug

Bug: AJAX tooltip for project tasks always shows "Access Forbidden" for restricted users

Summary

When hovering over a task link in /projet/tasks.php or /projet/tasks/list.php, the AJAX tooltip always shows the "Access Forbidden" page instead of task metadata, even for users who:

  • Have projet → lire permission, and
  • Are a contact of the project (project or task level).

Navigating directly to /projet/tasks/task.php?id=X works correctly. The bug is exclusively in the AJAX tooltip (core/ajax/ajaxtooltip.php).


Environment

  • Dolibarr version: 22.x (confirmed on 22.0.4)
  • PHP: 8.1+
  • Affected pages: /projet/tasks.php, /projet/tasks/list.php
  • Affected component: core/lib/security.lib.phprestrictedArea() / checkUserAccessToObject()

Steps to Reproduce

  1. Create a non-public project.
  2. Create at least one task inside it.
  3. Create an internal user with permission projet → lire but without projet → all → lire.
  4. Add that user as a project contact (PROJECTLEADER, PROJECTCONTRIBUTOR, etc.).
  5. Log in as that user.
  6. Go to /projet/tasks.php?id=<project_id> or /projet/tasks/list.php.
  7. Hover over any task link.

Expected: Tooltip shows task metadata (ref, label, dates, status).
Actual: Tooltip shows the "Access Forbidden / Accès refusé" page.


Root Cause Analysis

Call chain

ajaxtooltip.php loads the task object via fetchObjectByElement($id, 'project_task').
Inside fetchObjectByElement, when $objecttmp->module is empty, it is filled from getElementProperties():

// core/lib/functions.lib.php — fetchObjectByElement()
if (empty($objecttmp->module)) {
    $objecttmp->module = $element_prop['module']; // → 'projet' for project_task
}

Then ajaxtooltip.php calls:

$module = $object->module;                                       // 'projet'
$element = $object->element;                                     // 'project_task'
$usesublevelpermission = ($module != $element ? $element : ''); // 'project_task'
if ($usesublevelpermission && !$user->hasRight($module, $element)) {
    $usesublevelpermission = ''; // cleared — no right 'projet'→'project_task'
}
restrictedArea($user, $object->module, $object, $object->table_element, $usesublevelpermission);
// effectively: restrictedArea($user, 'projet', $task, 'projet_task', '')

The bug: wrong code path in checkUserAccessToObject

restrictedArea calls checkUserAccessToObject($user, ['projet'], $task, 'projet_task', [''], ...).

Inside checkUserAccessToObject, feature 'projet' matches $checkproject:

$checkproject = array('projet', 'project'); // line ~999
$checktask    = array('projet_task', 'project_task'); // line ~1000

// ...

if (in_array($feature, $checkproject) && $objectid > 0) {       // ← matches 'projet'
    if (isModEnabled('project') && !$user->hasRight('projet', 'all', 'lire')) {
        $projectid = $objectid; // ← BUG: objectid is the TASK id, not the PROJECT id
        $tmps = $projectstatic->getProjectsAuthorizedForUser($user, 0, 1, 0);
        $tmparray = explode(',', $tmps);
        if (!in_array($projectid, $tmparray)) { // task_id ∉ [project_ids] → always false
            return false; // ← ACCESS DENIED
        }
    }
}

$objectid is the task ID. getProjectsAuthorizedForUser() returns project IDs. The in_array check always fails → access always denied.

The $checktask block (a few lines below) handles this case correctly — it fetches the task and reads $task->fk_project to get the real project ID — but it is never reached because the feature 'projet' directs execution into $checkproject first.

The normalization at line ~974 would redirect to $checktask if $feature2 contained 'project_task':

if ($feature == 'projet' && !empty($feature2) && is_array($feature2)
    && !empty(array_intersect(array('project_task', 'projet_task'), $feature2))) {
    $feature = 'project_task'; // → would reach $checktask ✓
}

But $feature2 arrives empty because $usesublevelpermission was cleared in ajaxtooltip.php.

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------   

Fix

Single change in htdocs/core/lib/security.lib.php, in the restrictedArea() normalization section (after the // @todo check : project_task comment):

--- a/htdocs/core/lib/security.lib.php
+++ b/htdocs/core/lib/security.lib.php
@@ // @todo check : project_task
-// @todo possible ?
-// elseif (substr($features, -3, 3) == 'det') {
-// ...
-// }
+// When the object is a task (element='project_task') and $feature2 is empty,
+// $checkUserAccessToObject() falls into the $checkproject path and uses the task ID
+// as project ID, which always fails. Setting $feature2='project_task' triggers the
+// normalization at line 974 that redirects to the $checktask path, which correctly
+// resolves $task->fk_project before calling getProjectsAuthorizedForUser().
+if (is_object($object) && in_array($object->element, array('project_task', 'task'))
+    && (empty($features) || in_array($features, array('projet', 'project')))
+    && empty($feature2)) {
+    $features = 'projet';
+    $feature2 = 'project_task';
+    if (empty($tableandshare)) {
+        $tableandshare = 'projet_task';
+    }
+}

Why this works

With $feature2 = 'project_task', the flow becomes:

restrictedArea($user, 'projet', $task, 'projet_task', 'project_task')
  → [read check] $feature == 'projet' → checks projet->lire ✓
  → checkUserAccessToObject(['projet'], ..., ['project_task'])
    → line 974: feature='projet' + feature2 contains 'project_task' → feature = 'project_task'$checktask block:
        $task->fetch(task_id) → $projectid = $task->fk_project  ← correct project ID
        getProjectsAuthorizedForUser() → [authorized project IDs]
        in_array(real_project_id, authorized_ids) → true ✓

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------   

Related

┌─────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐
│ File                                │ Role                                                                     │
├─────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ htdocs/core/lib/security.lib.php    │ Fix here — restrictedArea() normalization + $checkproject bug            │
├─────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ htdocs/core/ajax/ajaxtooltip.php    │ Clears $usesublevelpermission before passing to restrictedArea           │
├─────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ htdocs/core/lib/functions.lib.php   │ fetchObjectByElement() sets $module='projet' from element properties     │
└─────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘



### Dolibarr Version

22.0.4

### Environment PHP

8.x

### Environment Database

mariadb

### Steps to reproduce the behavior and expected behavior

1. Enable the **Projects** module.
2. Create an internal user with the permission **`projet → lire`** (read projects) but **without** `projet → all → lire` (read all projects).
3. Create a project (visibility: private / not public).
4. Add the user from step 2 as a **project contact** (e.g. role PROJECTLEADER or PROJECTCONTRIBUTOR).
5. Create at least one task inside the project.
6. Log in as the user from step 2.
7. Navigate to **`/projet/tasks.php`** or **`/projet/tasks/list.php`**.
8. **Hover** over any task link.

**Expected result:** A tooltip appears showing the task metadata (reference, label, dates, status).
**Actual result:** The tooltip displays the "Access Forbidden / Accès refusé" page.

> Note: navigating directly to `/projet/tasks/task.php?id=X` works correctly — the bug is
> exclusive to the AJAX tooltip triggered on hover.

### Attached files

_No response_

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugThis is a bug (something does not work as expected)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions