-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Open
Labels
BugThis is a bug (something does not work as expected)This is a bug (something does not work as expected)
Description
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 → lirepermission, 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.php→restrictedArea()/checkUserAccessToObject()
Steps to Reproduce
- Create a non-public project.
- Create at least one task inside it.
- Create an internal user with permission
projet → lirebut withoutprojet → all → lire. - Add that user as a project contact (PROJECTLEADER, PROJECTCONTRIBUTOR, etc.).
- Log in as that user.
- Go to
/projet/tasks.php?id=<project_id>or/projet/tasks/list.php. - 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_Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugThis is a bug (something does not work as expected)This is a bug (something does not work as expected)