CVE-2024-37167 (published 2024-06-25T20:15Z)

Tuleap is an Open Source Suite to improve management of software developments and collaboration. Users are able to see backlog items that they should not see. This issue has been patched in Tuleap Community Edition version 15.9.99.97.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/Enalean/tuleap
commit_details: repo_url: https://github.com/Enalean/tuleap
author: Thomas Gerbet
author_date: 2024-06-07 14:33:32+02:00
msg: fix: request #38297 Permissions of the backlog items are not verified

Change-Id: I5ea5e5cb4e5d320e77cb174fda9c598fd4604963
repository: repo_url: https://github.com/Enalean/tuleap
repo_name: Enalean/tuleap
description: Tuleap is an Open Source Suite to improve management of software developments and collaboration. With a single web-based solution, project managers, developers & quality managers can easily build, deploy software projects.
repo_language: PHP
owner: Enalean
file_changes: filename: BacklogItemCollectionFactory.class.php
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@
 use Tuleap\AgileDashboard\ExplicitBacklog\ArtifactsInExplicitBacklogDao;
 use Tuleap\AgileDashboard\RemainingEffortValueRetriever;
 use Tuleap\Tracker\Artifact\Artifact;
+use Tuleap\Tracker\Permission\RetrieveUserPermissionOnArtifacts;
 
 /**
  * I build collections of IBacklogItem
@@ -95,6 +96,7 @@ class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactory
         RemainingEffortValueRetriever $remaining_effort_value_retriever,
         ArtifactsInExplicitBacklogDao $artifacts_in_explicit_backlog_dao,
         Tracker_Artifact_PriorityDao $artifact_priority_dao,
+        private readonly RetrieveUserPermissionOnArtifacts $user_permission_on_artifacts_retriever,
     ) {
         $this->dao                               = $dao;
         $this->artifact_factory                  = $artifact_factory;
@@ -185,10 +187,16 @@ class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactory
         Planning_Milestone $milestone,
         ?string $redirection_url,
     ): AgileDashboard_Milestone_Backlog_IBacklogItemCollection {
+        $permissions_on_items = $this->user_permission_on_artifacts_retriever->retrieveUserPermissionOnArtifacts(
+            $user,
+            iterator_to_array($item_collection),
+            \Tuleap\Tracker\Permission\ArtifactPermissionType::PERMISSION_VIEW
+        );
+
         $artifacts        = [];
         $backlog_item_ids = [];
 
-        foreach ($item_collection as $artifact) {
+        foreach ($permissions_on_items->allowed as $artifact) {
             $artifacts[$artifact->getId()] = $artifact;
             $backlog_item_ids[]            = $artifact->getId();
         }
@@ -234,20 +242,17 @@ class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactory
         $redirect_to_self,
     ): AgileDashboard_Milestone_Backlog_IBacklogItemCollection {
         $artifacts         = [];
-        $backlog_item_ids  = [];
         $sub_milestone_ids = $this->getSubmilestoneIds($user, $milestone);
 
         $item_collection = $backlog->getOpenUnplannedArtifacts($user, $sub_milestone_ids);
         foreach ($item_collection as $artifact) {
             $artifacts[$artifact->getId()] = $artifact;
-            $backlog_item_ids[]            = $artifact->getId();
         }
 
         $collection = $this->buildTopBacklogCollection(
             $user,
             $milestone,
             $redirect_to_self,
-            $backlog_item_ids,
             $artifacts
         );
         $collection->setTotalAvaialableSize($item_collection->getTotalAvaialableSize());
@@ -713,22 +718,19 @@ class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactory
         );
         $collection_total_size = $this->artifacts_in_explicit_backlog_dao->foundRows();
 
-        $backlog_item_ids = [];
-        $artifacts        = [];
+        $artifacts = [];
         foreach ($rows as $row) {
             $artifact = $this->artifact_factory->getArtifactById($row['artifact_id']);
             if ($artifact === null) {
                 continue;
             }
             $artifacts[$artifact->getId()] = $artifact;
-            $backlog_item_ids[]            = $artifact->getId();
         }
 
         $collection = $this->buildTopBacklogCollection(
             $user,
             $milestone,
             $redirect_to_self,
-            $backlog_item_ids,
             $artifacts
         );
         $collection->setTotalAvaialableSize($collection_total_size);
@@ -736,13 +738,26 @@ class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactory
         return $collection;
     }
 
+    /**
+     * @param Artifact[] $open_artifacts
+     */
     private function buildTopBacklogCollection(
         PFUser $user,
         Planning_Milestone $milestone,
         ?string $redirection_url,
-        array $backlog_item_ids,
         array $open_artifacts,
     ): AgileDashboard_Milestone_Backlog_IBacklogItemCollection {
+        $permissions_on_items = $this->user_permission_on_artifacts_retriever->retrieveUserPermissionOnArtifacts(
+            $user,
+            $open_artifacts,
+            \Tuleap\Tracker\Permission\ArtifactPermissionType::PERMISSION_VIEW,
+        );
+
+        $backlog_item_ids = [];
+        foreach ($permissions_on_items->allowed as $artifact) {
+            $backlog_item_ids[] = $artifact->getId();
+        }
+
         $parents   = $this->getParentArtifacts($milestone, $user, $backlog_item_ids);
         $semantics = $this->getArtifactsSemantics($user, $milestone, $backlog_item_ids, $open_artifacts);
 
@@ -753,7 +768,7 @@ class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactory
         }
 
         $collection = $this->backlog_item_builder->getCollection();
-        foreach ($open_artifacts as $artifact) {
+        foreach ($permissions_on_items->allowed as $artifact) {
             $artifact_id = $artifact->getId();
 
             if (! isset($semantics[$artifact_id])) {

programming_language: PHP
filename: ContentForMiletoneProvider.php
change_type: ModificationType.MODIFY
diff: @@ -75,7 +75,8 @@ class ContentForMiletoneProvider
                     Tracker_FormElementFactory::instance()
                 ),
                 new ArtifactsInExplicitBacklogDao(),
-                new Tracker_Artifact_PriorityDao()
+                new Tracker_Artifact_PriorityDao(),
+                \Tuleap\Tracker\Permission\TrackersPermissionsRetriever::build(),
             )
         );
     }

programming_language: PHP
filename: MilestoneResource.class.php
change_type: ModificationType.MODIFY
diff: @@ -160,7 +160,8 @@ class MilestoneResource extends AuthenticatedResource
             new AgileDashboard_Milestone_Backlog_BacklogItemBuilder(),
             new RemainingEffortValueRetriever(Tracker_FormElementFactory::instance()),
             new ArtifactsInExplicitBacklogDao(),
-            new Tracker_Artifact_PriorityDao()
+            new Tracker_Artifact_PriorityDao(),
+            \Tuleap\Tracker\Permission\TrackersPermissionsRetriever::build(),
         );
 
         $this->milestone_validator = new MilestoneResourceValidator(

programming_language: PHP
filename: ProjectBacklogResource.class.php
change_type: ModificationType.MODIFY
diff: @@ -119,7 +119,8 @@ class ProjectBacklogResource
                 $tracker_form_element_factory
             ),
             new ArtifactsInExplicitBacklogDao(),
-            new Tracker_Artifact_PriorityDao()
+            new Tracker_Artifact_PriorityDao(),
+            \Tuleap\Tracker\Permission\TrackersPermissionsRetriever::build(),
         );
 
         $this->milestone_validator = new MilestoneResourceValidator(

programming_language: PHP
filename: ProjectBacklogResource.class.php
change_type: ModificationType.MODIFY
diff: @@ -93,7 +93,8 @@ class ProjectBacklogResource
                 $tracker_form_element_factory
             ),
             new ArtifactsInExplicitBacklogDao(),
-            new \Tracker_Artifact_PriorityDao()
+            new \Tracker_Artifact_PriorityDao(),
+            \Tuleap\Tracker\Permission\TrackersPermissionsRetriever::build(),
         );
 
         $this->parent_tracker_retriever = new ParentTrackerRetriever($this->planning_factory);

programming_language: PHP
filename: agiledashboardPlugin.php
change_type: ModificationType.MODIFY
diff: @@ -1335,7 +1335,8 @@ class AgileDashboardPlugin extends Plugin implements PluginWithConfigKeys, Plugi
             $presenter_builder,
             new RemainingEffortValueRetriever($form_element_factory),
             new ArtifactsInExplicitBacklogDao(),
-            new Tracker_Artifact_PriorityDao()
+            new Tracker_Artifact_PriorityDao(),
+            \Tuleap\Tracker\Permission\TrackersPermissionsRetriever::build(),
         );
     }
 

programming_language: Perl
filename: AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactoryTest.php
change_type: ModificationType.MODIFY
diff: @@ -47,6 +47,7 @@ use Tuleap\Test\PHPUnit\TestCase;
 use Tuleap\Tracker\Artifact\Artifact;
 use Tuleap\Tracker\Test\Builders\ArtifactTestBuilder;
 use Tuleap\Tracker\Test\Builders\TrackerTestBuilder;
+use Tuleap\Tracker\Test\Stub\Tracker\Permission\TrackersPermissionsPassthroughRetriever;
 
 final class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactoryTest extends TestCase //phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
 {
@@ -83,6 +84,7 @@ final class AgileDashboard_Milestone_Backlog_BacklogItemCollectionFactoryTest ex
                 $this->remaining_effort_value_retriever,
                 $this->artifacts_in_explicit_backlog_dao,
                 $this->artifact_priority_dao,
+                new TrackersPermissionsPassthroughRetriever(),
             ])
             ->onlyMethods([
                 'userCanReadBacklogTitleField',

programming_language: PHP
filename: Pane.class.php
change_type: ModificationType.MODIFY
diff: @@ -225,7 +225,8 @@ class Cardwall_Pane extends AgileDashboard_Pane
                 $this->tracker_form_element_factory
             ),
             new ArtifactsInExplicitBacklogDao(),
-            new Tracker_Artifact_PriorityDao()
+            new Tracker_Artifact_PriorityDao(),
+            \Tuleap\Tracker\Permission\TrackersPermissionsRetriever::build(),
         );
 
         $backlog_factory = new AgileDashboard_Milestone_Backlog_BacklogFactory(

programming_language: PHP
filename: ProjectMilestonesPresenterBuilder.php
change_type: ModificationType.MODIFY
diff: @@ -169,7 +169,8 @@ class ProjectMilestonesPresenterBuilder
                 new AgileDashboard_Milestone_Backlog_BacklogItemBuilder(),
                 new RemainingEffortValueRetriever(Tracker_FormElementFactory::instance()),
                 new ArtifactsInExplicitBacklogDao(),
-                new Tracker_Artifact_PriorityDao()
+                new Tracker_Artifact_PriorityDao(),
+                \Tuleap\Tracker\Permission\TrackersPermissionsRetriever::build(),
             ),
             $milestone_factory,
             new ExplicitBacklogDao(),

programming_language: PHP
filename: TrackersPermissionsRetriever.php
change_type: ModificationType.MODIFY
diff: @@ -201,10 +201,6 @@ final readonly class TrackersPermissionsRetriever implements RetrieveUserPermiss
 
     public function retrieveUserPermissionOnArtifacts(PFUser $user, array $artifacts, ArtifactPermissionType $permission): UserPermissionsOnItems
     {
-        if (! self::isEnabled()) {
-            throw new LogicException('Trackers permissions on tracker are disabled by feature flag.');
-        }
-
         if ($artifacts === []) {
             return new UserPermissionsOnItems($user, $permission, [], []);
         }

programming_language: PHP
filename: TrackersPermissionsPassthroughRetriever.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,59 @@
+<?php
+/**
+ * Copyright (c) Enalean, 2024-Present. All Rights Reserved.
+ *
+ * This file is a part of Tuleap.
+ *
+ * Tuleap 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.
+ *
+ * Tuleap 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 Tuleap. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+declare(strict_types=1);
+
+namespace Tuleap\Tracker\Test\Stub\Tracker\Permission;
+
+use PFUser;
+use Tuleap\Tracker\Permission\ArtifactPermissionType;
+use Tuleap\Tracker\Permission\FieldPermissionType;
+use Tuleap\Tracker\Permission\RetrieveUserPermissionOnArtifacts;
+use Tuleap\Tracker\Permission\RetrieveUserPermissionOnFields;
+use Tuleap\Tracker\Permission\RetrieveUserPermissionOnTrackers;
+use Tuleap\Tracker\Permission\TrackerPermissionType;
+use Tuleap\Tracker\Permission\UserPermissionsOnItems;
+
+final readonly class TrackersPermissionsPassthroughRetriever implements RetrieveUserPermissionOnFields, RetrieveUserPermissionOnTrackers, RetrieveUserPermissionOnArtifacts
+{
+    public function retrieveUserPermissionOnArtifacts(
+        PFUser $user,
+        array $artifacts,
+        ArtifactPermissionType $permission,
+    ): UserPermissionsOnItems {
+        return new UserPermissionsOnItems($user, $permission, $artifacts, []);
+    }
+
+    public function retrieveUserPermissionOnFields(
+        PFUser $user,
+        array $fields,
+        FieldPermissionType $permission,
+    ): UserPermissionsOnItems {
+        return new UserPermissionsOnItems($user, $permission, $fields, []);
+    }
+
+    public function retrieveUserPermissionOnTrackers(
+        PFUser $user,
+        array $trackers,
+        TrackerPermissionType $permission,
+    ): UserPermissionsOnItems {
+        return new UserPermissionsOnItems($user, $permission, $trackers, []);
+    }
+}

programming_language: PHP