CVE-2024-23344 (PHP, published 2024-02-06T16:15Z)
Description: value: Tuleap is an Open Source Suite to improve management of software developments and collaboration. Some users might get access to restricted information when a process validates the permissions of multiple users (e.g. mail notifications). This issue has been patched in version 15.4.99.140 of Tuleap Community Edition.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/Enalean/tuleap
Files changed:
plugins/agiledashboard/tests/unit/AgileDashboard/Milestone/Sidebar/PromotedMilestoneBuilderTest.php
plugins/agiledashboard/tests/unit/AgileDashboard/Planning/MilestoneFactoryGetBareMilestoneByArtifactIdTest.php
plugins/agiledashboard/tests/unit/AgileDashboard/Planning/MilestoneFactoryGetMilestoneTest.php
plugins/agiledashboard/tests/unit/AgileDashboard/Planning/MilestoneFactoryGetPaginatedMilestonesTest.php
plugins/agiledashboard/tests/unit/AgileDashboard/Planning/MilestoneFactoryGetTopMilestonesTest.php
plugins/agiledashboard/tests/unit/AgileDashboard/Planning/MilestoneFactoryTest.php
plugins/roadmap/tests/unit/Roadmap/REST/v1/IterationsRetrieverTest.php
plugins/roadmap/tests/unit/Roadmap/REST/v1/RoadmapTasksRetrieverTest.php
plugins/tracker/include/Tracker/Artifact/Artifact.php
plugins/tracker/tests/unit/Builders/ArtifactTestBuilder.php
plugins/tracker/tests/unit/Tracker/Artifact/ArtifactTest.php
plugins/tracker/tests/unit/Tracker/Semantic/Timeframe/TimeframeNotConfiguredTest.php
plugins/tracker/tests/unit/Tracker/Semantic/Timeframe/TimeframeWithDurationTest.php
plugins/tracker/tests/unit/Tracker/Semantic/Timeframe/TimeframeWithEndDateTest.php
Commit message: fix: CCE user cannot update artifact sometimes

How to reproduce:
Have an artifact A with a comment containing a cross reference to another
artifact B like `See art #123`.
In the tracker of the artifact A, upload a Custom Code Execution module
in the administration.

There is an example module if you need one:
```
cd plugins/tracker_cce/wasm_modules/post-action-add-comment
nix-shell
cargo build --target wasm32-wasi --release
```
Then you can upload the generated file:
`plugins/tracker_cce/wasm_modules/post-action-add-comment/target/wasm32-wasi/release/post-action-add-comment.wasm`

The example module take an integer field `field_a`, an integer field
`field_b`, and fill a text field `field_sum`. You will need them for the
test. Or use your own module.

Now update the artifact A. The (async) execution of the module should be a
success and a new comment is added to the artifact by CCE user.

Closes request #35862: Artifact userCanView not properly cached
Part of story #35093: execute custom code as artifact post action

Change-Id: Id08ce6570c1dd2c7ebedcd70339f9d9d6b008d58


Vulnerable code:
            ->userCanView(true)
            ->userCanView(false)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
        $iteration = $this->anArtifactWithoutChangesetValue(201, 'Title', $this->tracker, true);
        $iteration = $this->anArtifact(123, "Sprint W42", $this->tracker, true);
    private function anArtifact(int $id, string $title, Tracker $tracker, bool $readable): Artifact
            ->userCanView($readable)
    private function anArtifactWithoutChangesetValue(int $id, string $title, Tracker $tracker, bool $readable): Artifact
            ->userCanView($readable)
        $task_203 = $this->anArtifactWithoutStartDate(203, 'Do those', $tracker, true, $semantic_timeframe);
        $task_203 = $this->anArtifactWithoutStartDate(203, 'Do those', $tracker, true, $semantic_timeframe);
        $task_203 = $this->anArtifactWithoutStartDate(203, 'Do those', $another_tracker, true, $semantic_timeframe_another_tracker);
        return ArtifactTestBuilder::anArtifact($id)
            ->userCanView($readable)
            ->isOpen(true)
            ->build();
    private function anArtifactWithoutStartDate(int $id, string $title, Tracker $tracker, bool $readable, SemanticTimeframe $semantic_timeframe): Artifact
            ->userCanView($readable)
    private bool | null $user_permission_cache = null;
    private ?Artifact $parent                  = null;
    private bool $has_set_parent               = false;
    private bool | null $is_open               = null;
    public function setUserCanView(bool $user_can_view): void
        if ($this->user_permission_cache !== null) {
            return $this->user_permission_cache;
        $this->user_permission_cache = PermissionsCache::userCanView($this, $user, $permission_checker);

        return $this->user_permission_cache;
    private bool|null $user_can_view = null;
    public function userCanView(bool $user_can_view): self
        $this->user_can_view = $user_can_view;
        if ($this->user_can_view !== null) {
            $artifact->setUserCanView($this->user_can_view);
        }
            ->userCanView(true)
            UserTestBuilder::anActiveUser()->build(),
            ->userCanView(true)
            UserTestBuilder::anActiveUser()->build(),
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)
            ->userCanView(true)

Fixed code:
            ->userCanView($this->user)
            ->userCannotView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
        $iteration = $this->anArtifactWithoutChangesetValue(201, 'Title', $this->tracker);
        $iteration = $this->anArtifact(123, "Sprint W42", $this->tracker);
    private function anArtifact(int $id, string $title, Tracker $tracker): Artifact
            ->userCanView($this->user)
    private function anArtifactWithoutChangesetValue(int $id, string $title, Tracker $tracker): Artifact
            ->userCanView($this->user)
        $task_203 = $this->anArtifactWithoutStartDate(203, 'Do those', $tracker, $semantic_timeframe);
        $task_203 = $this->anArtifactWithoutStartDate(203, 'Do those', $tracker, $semantic_timeframe);
        $task_203 = $this->anArtifactWithoutStartDate(203, 'Do those', $another_tracker, $semantic_timeframe_another_tracker);
        $artifact_test_builder = ArtifactTestBuilder::anArtifact($id)
            ->isOpen(true);

        return $readable
            ? $artifact_test_builder->userCanView($this->user)->build()
            : $artifact_test_builder->userCannotView($this->user)->build();
    private function anArtifactWithoutStartDate(int $id, string $title, Tracker $tracker, SemanticTimeframe $semantic_timeframe): Artifact
            ->userCanView($this->user)
    /**
     * @var array<int, bool>
     */
    private array $user_permission_cache = [];
    private ?Artifact $parent            = null;
    private bool $has_set_parent         = false;
    private bool | null $is_open         = null;
    /**
     * @param array<int, bool> $user_can_view
     */
    public function setUserCanView(array $user_can_view): void
        if ($user && isset($this->user_permission_cache[(int) $user->getId()])) {
            return $this->user_permission_cache[(int) $user->getId()];
        if (! isset($this->user_permission_cache[(int) $user->getId()])) {
            $this->user_permission_cache[(int) $user->getId()] = PermissionsCache::userCanView($this, $user, $permission_checker);
        }
        return $this->user_permission_cache[(int) $user->getId()];

    /**
     * @var array<int, bool>
     */
    private array $user_can_view = [];
    public function userCanView(PFUser $user): self
    {
        $this->user_can_view[$user->getId()] = true;
        return $this;
    }

    public function userCannotView(PFUser $user): self
        $this->user_can_view[$user->getId()] = false;
        $artifact->setUserCanView($this->user_can_view);
<?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\Artifact;

use Tuleap\Test\Builders\UserTestBuilder;
use Tuleap\Test\PHPUnit\TestCase;

final class ArtifactTest extends TestCase
{
    public function testUserCanViewCache(): void
    {
        $artifact = new Artifact(1, 2, 3, 4, false);

        $bob   = UserTestBuilder::anActiveUser()->withId(1)->build();
        $alice = UserTestBuilder::anActiveUser()->withId(2)->build();

        $artifact->setUserCanView([
            $bob->getId() => false,
            $alice->getId() => true,
        ]);

        self::assertFalse($artifact->userCanView($bob));
        self::assertTrue($artifact->userCanView($alice));
    }
}
        $user = UserTestBuilder::anActiveUser()->build();

            ->userCanView($user)
            $user,
        $user = UserTestBuilder::anActiveUser()->build();

            ->userCanView($user)
            $user,
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)
            ->userCanView($this->user)