CVE-2024-25130 (PHP, published 2024-02-22T19:15Z)
Description: value: Tuleap is an open source suite to improve management of software developments and collaboration. Prior to version 15.5.99.76 of Tuleap Community Edition and prior to versions 15.5-4 and 15.4-7 of Tuleap Enterprise Edition, users with a read access to a tracker where the mass update feature is used might get access to restricted information. Tuleap Community Edition 15.5.99.76, Tuleap Enterprise Edition 15.5-4, and Tuleap Enterprise Edition 15.4-7 contain a patch for this issue.
Severity: nan (CVSS v3 base score: )
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/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_PermissionsOnArtifact.php
plugins/tracker/include/Tracker/MasschangeDataValueExtractor.class.php
plugins/tracker/scripts/legacy/src/TrackerFormElementFieldPermissions.js
plugins/tracker/site-content/fr_FR/LC_MESSAGES/tuleap-tracker.po
plugins/tracker/site-content/pt_BR/LC_MESSAGES/tuleap-tracker.po
plugins/tracker/templates/form-element/permissions_on_artifact_massupdate.mustache
plugins/tracker/tests/unit/Tracker/MasschangeDataValueExtractorTest.php
Commit message: fix: request #36803 Mass update clears permissions on artifact

An additional checkbox is added to control if the field should be
updated or not when doing a masschange.

Change-Id: I721298a0f5ec50c3aac1a59efcfba6166958baf0


Vulnerable code:
    public const GRANTED_GROUPS     = 'granted_groups';
    public const USE_IT             = 'use_artifact_permissions';
    public const IS_USED_BY_DEFAULT = false;
    public const PERMISSION_TYPE    = 'PLUGIN_TRACKER_ARTIFACT_ACCESS';
    /**
     * @return string
     */
    protected function fetchSubmitValueMasschange()
        return $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
    private function fetchUserGroupList($is_read_only, array $changeset_values)
            . 'id="' . $hp->purify(str_replace('[]', '', $element_name)) . '" '
     *
     * @return string
    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input)
        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '">';
                $html .= '<input type="hidden" name="artifact[' . $this->getId() . '][use_artifact_permissions]" value="0" />';
            $html .= '<label class="checkbox" for="artifact_' . $this->getId() . '_use_artifact_permissions">';
            $html .= '<input type="checkbox"
                        name="artifact[' . $this->getId() . '][use_artifact_permissions]"
                        id="artifact_' . $this->getId() . '_use_artifact_permissions"
            $html .= '<input type="hidden" name="artifact[' . $this->getId(
            ) . '][use_artifact_permissions]" value="1" />';
 * This file is a part of Codendi.
 * Codendi is free software; you can redistribute it and/or modify
 * Codendi is distributed in the hope that it will be useful,
 * along with Codendi; if not, write to the Free Software
/* global jQuery:readonly */

(function ($) {
        $(".tracker_field_permissionsonartifact").each(function (index, element) {
            disablePermissionsFieldIfNeeded(element);
            bindSwitch(element);
        });
    function disablePermissionsFieldIfNeeded(element) {
        if ($("input[type='checkbox']", element).is(":not(:checked)")) {
            $(element).siblings("select").attr("disabled", "disabled");
    function bindSwitch(element) {
        $("input[type='checkbox']", element).bind("change", function () {
            togglePermissionsField(element);
    function togglePermissionsField(element) {
        if ($("input[type='checkbox']", element).is(":not(:checked)")) {
            $(element).siblings("select").attr("disabled", "disabled");
        } else {
            $(element).siblings("select").removeAttr("disabled");
        }
    }

    $(document).ready(function () {
        init();
    });
})(jQuery);
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
    use MockeryPHPUnitIntegration;
    public function testItReturnsFieldWithItNewValue(): void
        $form_element_factory           = Mockery::mock(Tracker_FormElementFactory::class);
        $masschange_data_values_manager = new Tracker_MasschangeDataValueExtractor($form_element_factory);
        $text_field_1    = Mockery::mock(Tracker_FormElement_Field_Text::class);
        $text_field_1_id = 1;
        $text_field_2    = Mockery::mock(Tracker_FormElement_Field_Text::class);
        $text_field_2_id = 2;
        $list_field_1    = Mockery::mock(Tracker_FormElement_Field_List::class);
        $list_field_1_id = 3;

        $list_field_2    = Mockery::mock(Tracker_FormElement_Field_List::class);
        $list_field_2_id = 4;

        $form_element_factory->shouldReceive('getFieldById')->withArgs([$text_field_1_id])->andReturn($text_field_1);
        $form_element_factory->shouldReceive('getFieldById')->withArgs([$text_field_2_id])->andReturn($text_field_2);
        $form_element_factory->shouldReceive('getFieldById')->withArgs([$list_field_1_id])->andReturn($list_field_1);
        $form_element_factory->shouldReceive('getFieldById')->withArgs([$list_field_2_id])->andReturn($list_field_2);
        $masschange_data = [
            $text_field_1_id => 'Unchanged',
            $text_field_2_id => 'Value01',
            $list_field_1_id => ['-1'],
            $list_field_2_id => ['Value02'],
        ];

        $expected_result = [
            $text_field_2_id => 'Value01',
            $list_field_2_id => ['Value02'],
        ];
            $masschange_data_values_manager->getNewValues($masschange_data)

Fixed code:
    public const GRANTED_GROUPS      = 'granted_groups';
    public const USE_IT              = 'use_artifact_permissions';
    public const DO_MASS_UPDATE_FLAG = 'do_mass_update';
    public const IS_USED_BY_DEFAULT  = false;
    public const PERMISSION_TYPE     = 'PLUGIN_TRACKER_ARTIFACT_ACCESS';
    protected function fetchSubmitValueMasschange(): string
        $mass_change_input_html = TemplateRendererFactory::build()->getRenderer(__DIR__ . '/../../../templates/form-element/')
            ->renderToString(
                'permissions_on_artifact_massupdate',
                ['field_id' => $this->getId(), 'flag_mass_update' => self::DO_MASS_UPDATE_FLAG]
            );

        return $mass_change_input_html . $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
    private function fetchUserGroupList($is_read_only, array $changeset_values): string
            . 'id="' . $hp->purify('artifact_' . $field_id . '_perms_ugroups' . ($is_read_only ? '_ro' : '')) . '" '
    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input): string
        $field_id = Codendi_HTMLPurifier::instance()->purify($this->getId());

        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '" data-field-id="' . $field_id . '">';
                $html .= '<input type="hidden" name="artifact[' . $field_id . '][use_artifact_permissions]" value="0" />';
            $read_only_id = $disabled ? '_ro' : '';
            $html        .= '<label class="checkbox" for="artifact_' . $field_id . '_use_artifact_permissions' . $read_only_id . '">';
            $html        .= '<input type="checkbox"
                        name="artifact[' . $field_id . '][use_artifact_permissions]"
                        id="artifact_' . $field_id . '_use_artifact_permissions' . $read_only_id . '"
            $html .= '<input type="hidden" name="artifact[' . $field_id . '][use_artifact_permissions]" value="1" />';
        } elseif ($field instanceof Tracker_FormElement_Field_PermissionsOnArtifact) {
            return isset($data[Tracker_FormElement_Field_PermissionsOnArtifact::DO_MASS_UPDATE_FLAG]) &&
                $data[Tracker_FormElement_Field_PermissionsOnArtifact::DO_MASS_UPDATE_FLAG] === '1';
 * 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
 * Tuleap is distributed in the hope that it will be useful,
 * along with Tuleap; if not, write to the Free Software
document.addEventListener("DOMContentLoaded", function () {
        const elements = document.getElementsByClassName("tracker_field_permissionsonartifact");
        for (const element of elements) {
            const field_id = element.dataset.fieldId;
            const select_ugroups = document.getElementById(`artifact_${field_id}_perms_ugroups`);
            const checkbox_use_perms = document.getElementById(
                `artifact_${field_id}_use_artifact_permissions`,
            );
            if (select_ugroups === null || checkbox_use_perms === null) {
                continue;
            }
            disablePermissionsFieldIfNeeded(checkbox_use_perms, select_ugroups);
            bindSwitch(checkbox_use_perms, select_ugroups);
            const checkbox_activate_masschange_update = document.getElementById(
                `artifact_${field_id}_perms_mass_update`,
            );
            if (checkbox_activate_masschange_update) {
                checkbox_use_perms.disabled = true;
                checkbox_activate_masschange_update.addEventListener("change", () => {
                    checkbox_use_perms.disabled = !checkbox_activate_masschange_update.checked;
                });
            }
        }
    function disablePermissionsFieldIfNeeded(checkbox_use_perms, select_ugroups) {
        if (!checkbox_use_perms.checked) {
            select_ugroups.disabled = true;
    function bindSwitch(checkbox_use_perms, select_ugroups) {
        checkbox_use_perms.addEventListener("change", () => {
            select_ugroups.disabled = !checkbox_use_perms.checked;
    init();
});
msgid "Update the permissions, by default nothing is changed"
msgstr "Mettre à jour les permissions, par défaut rien ne change"

msgid "Update the permissions, by default nothing is changed"
msgstr ""

<label class="checkbox" for="artifact_{{ field_id }}_perms_mass_update">
    <input type="checkbox"
        name="artifact[{{ field_id }}][{{ flag_mass_update }}]"
        id="artifact_{{ field_id }}_perms_mass_update"
        value="1"
    />
    {{# dgettext }} tuleap-tracker | Update the permissions, by default nothing is changed {{/ dgettext }}
</label>
use Tracker_FormElement_Field_PermissionsOnArtifact;
    /**
     * @dataProvider dataProviderFields
     * @param class-string $field_class
     */
    public function testReturnsFieldWithNewValue(string $field_class, mixed $value, bool $is_expected_to_set_value): void
        $field = $this->createStub($field_class);
        $form_element_factory = $this->createStub(Tracker_FormElementFactory::class);
        $form_element_factory->method('getFieldById')->willReturn($field);
        $masschange_data = [12 => $value];
        $expected_result = $is_expected_to_set_value ? $masschange_data : [];
        $masschange_data_values_extractor = new Tracker_MasschangeDataValueExtractor($form_element_factory);
            $masschange_data_values_extractor->getNewValues($masschange_data)

    public function dataProviderFields(): array
    {
        return [
            'Field with an update' => [
                Tracker_FormElement_Field_Text::class,
                'Value01',
                true,
            ],
            'Field without an update' => [
                Tracker_FormElement_Field_Text::class,
                'Unchanged',
                false,
            ],
            'List field with an update' => [
                Tracker_FormElement_Field_List::class,
                ['Value02'],
                true,
            ],
            'List field without an update' => [
                Tracker_FormElement_Field_List::class,
                ['-1'],
                false,
            ],
            'Permissions on artifact field with an update' => [
                Tracker_FormElement_Field_PermissionsOnArtifact::class,
                ['do_mass_update' => '1', 'use_artifact_permissions' => '1'],
                true,
            ],
            'Permissions on artifact field without an update' => [
                Tracker_FormElement_Field_PermissionsOnArtifact::class,
                ['use_artifact_permissions' => '1'],
                false,
            ],
        ];
    }