CVE-2024-25130 (published 2024-02-22T19:15Z)

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.


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-02-13 10:38:51+01:00
msg: 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
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: Tracker_FormElement_Field_PermissionsOnArtifact.php
change_type: ModificationType.MODIFY
diff: @@ -36,10 +36,11 @@ use Tuleap\User\UserGroup\NameTranslator;
 //phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNamespace, Squiz.Classes.ValidClassName.NotCamelCaps
 class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElement_Field
 {
-    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';
+    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';
 
     public $default_properties = [];
 
@@ -154,15 +155,18 @@ class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElemen
         return $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
     }
 
-    /**
-     * @return string
-     */
-    protected function fetchSubmitValueMasschange()
+    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]
+            );
+
         $is_checked  = false;
         $is_disabled = false;
 
-        return $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
+        return $mass_change_input_html . $this->getArtifactValueHTML($this->getId(), $is_checked, $is_disabled);
     }
 
     /**
@@ -262,7 +266,7 @@ class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElemen
         return $html;
     }
 
-    private function fetchUserGroupList($is_read_only, array $changeset_values)
+    private function fetchUserGroupList($is_read_only, array $changeset_values): string
     {
         $field_id     = $this->getId();
         $element_name = 'artifact[' . $field_id . '][u_groups][]';
@@ -270,7 +274,7 @@ class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElemen
         $hp    = Codendi_HTMLPurifier::instance();
         $html  = '<select '
             . 'name="' . $hp->purify($element_name) . '" '
-            . 'id="' . $hp->purify(str_replace('[]', '', $element_name)) . '" '
+            . 'id="' . $hp->purify('artifact_' . $field_id . '_perms_ugroups' . ($is_read_only ? '_ro' : '')) . '" '
             . 'multiple '
             . 'size="8" '
             . (($this->isRequired()) ? 'required="required"' : '' )
@@ -852,32 +856,32 @@ class Tracker_FormElement_Field_PermissionsOnArtifact extends Tracker_FormElemen
     /**
      * @param bool $can_user_restrict_permissions_to_nobody
      * @param bool $disabled
-     *
-     * @return string
      */
-    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input)
+    private function fetchRestrictCheckbox($can_user_restrict_permissions_to_nobody, $disabled, $is_expecting_input): string
     {
         $empty_value_class = '';
         if ($is_expecting_input) {
             $empty_value_class = 'empty_value';
         }
 
-        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '">';
+        $field_id = Codendi_HTMLPurifier::instance()->purify($this->getId());
+
+        $html = '<p class="tracker_field_permissionsonartifact ' . $empty_value_class . '" data-field-id="' . $field_id . '">';
         if ($this->isRequired() == false) {
             if (! $disabled) {
-                $html .= '<input type="hidden" name="artifact[' . $this->getId() . '][use_artifact_permissions]" value="0" />';
+                $html .= '<input type="hidden" name="artifact[' . $field_id . '][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"
+            $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 . '"
                         value="1" ' .
                 (($can_user_restrict_permissions_to_nobody == true) ? 'checked="checked"' : '') .
                 (($disabled == true) ? 'disabled="disabled"' : '') .
                 '/>';
         } else {
-            $html .= '<input type="hidden" name="artifact[' . $this->getId(
-            ) . '][use_artifact_permissions]" value="1" />';
+            $html .= '<input type="hidden" name="artifact[' . $field_id . '][use_artifact_permissions]" value="1" />';
         }
 
         $html .= dgettext('tuleap-tracker', 'Restrict access to this artifact for the following user groups:') . '</label>';

programming_language: PHP
filename: MasschangeDataValueExtractor.class.php
change_type: ModificationType.MODIFY
diff: @@ -50,6 +50,9 @@ class Tracker_MasschangeDataValueExtractor
         $field = $this->form_element_factory->getFieldById($field_id);
         if ($field instanceof Tracker_FormElement_Field_List) {
             return $this->isValueInData($data, (string) BindStaticValueUnchanged::VALUE_ID);
+        } 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';
         }
 
         return $this->isValueInData($data, $GLOBALS['Language']->getText('global', 'unchanged'));

programming_language: PHP
filename: TrackerFormElementFieldPermissions.js
change_type: ModificationType.MODIFY
diff: @@ -1,58 +1,65 @@
 /**
+ * Copyright (c) Enalean, 2024 - present. All Rights Reserved.
  * Copyright (c) Xerox Corporation, Codendi Team, 2001-2008. All rights reserved
  *
  * Originally written by Nicolas Terray, 2008
  *
- * This file is a part of Codendi.
+ * This file is a part of Tuleap.
  *
- * Codendi is free software; you can redistribute it and/or modify
+ * 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.
  *
- * Codendi is distributed in the hope that it will be useful,
+ * 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 Codendi; if not, write to the Free Software
+ * along with Tuleap; if not, write to the Free Software
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  *
  *
  */
 
-/* global jQuery:readonly */
-
-(function ($) {
+document.addEventListener("DOMContentLoaded", function () {
     function init() {
-        $(".tracker_field_permissionsonartifact").each(function (index, element) {
-            disablePermissionsFieldIfNeeded(element);
-            bindSwitch(element);
-        });
+        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(element) {
-        if ($("input[type='checkbox']", element).is(":not(:checked)")) {
-            $(element).siblings("select").attr("disabled", "disabled");
+    function disablePermissionsFieldIfNeeded(checkbox_use_perms, select_ugroups) {
+        if (!checkbox_use_perms.checked) {
+            select_ugroups.disabled = true;
         }
     }
 
-    function bindSwitch(element) {
-        $("input[type='checkbox']", element).bind("change", function () {
-            togglePermissionsField(element);
+    function bindSwitch(checkbox_use_perms, select_ugroups) {
+        checkbox_use_perms.addEventListener("change", () => {
+            select_ugroups.disabled = !checkbox_use_perms.checked;
         });
     }
 
-    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);
+    init();
+});

programming_language: JavaScript
filename: tuleap-tracker.po
change_type: ModificationType.MODIFY
diff: @@ -4294,6 +4294,9 @@ msgstr "Modifier le rappel"
 msgid "Update semantic"
 msgstr "Mettre à jour la sémantique"
 
+msgid "Update the permissions, by default nothing is changed"
+msgstr "Mettre à jour les permissions, par défaut rien ne change"
+
 msgid "Update to overridable computed field"
 msgstr "Mettre à jour vers champ calculé modifiable"
 

programming_language: CoffeeScript
filename: tuleap-tracker.po
change_type: ModificationType.MODIFY
diff: @@ -3786,6 +3786,9 @@ msgstr ""
 msgid "Update semantic"
 msgstr ""
 
+msgid "Update the permissions, by default nothing is changed"
+msgstr ""
+
 msgid "Update to overridable computed field"
 msgstr ""
 

programming_language: CoffeeScript
filename: permissions_on_artifact_massupdate.mustache
change_type: ModificationType.ADD
diff: @@ -0,0 +1,8 @@
+<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>

programming_language: Shell
filename: MasschangeDataValueExtractorTest.php
change_type: ModificationType.MODIFY
diff: @@ -22,9 +22,8 @@ declare(strict_types=1);
 
 namespace Tuleap\Tracker;
 
-use Mockery;
-use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
 use Tracker_FormElement_Field_List;
+use Tracker_FormElement_Field_PermissionsOnArtifact;
 use Tracker_FormElement_Field_Text;
 use Tracker_FormElementFactory;
 use Tracker_MasschangeDataValueExtractor;
@@ -32,48 +31,66 @@ use Tuleap\GlobalLanguageMock;
 
 final class MasschangeDataValueExtractorTest extends \Tuleap\Test\PHPUnit\TestCase
 {
-    use MockeryPHPUnitIntegration;
     use GlobalLanguageMock;
 
-    public function testItReturnsFieldWithItNewValue(): void
+    /**
+     * @dataProvider dataProviderFields
+     * @param class-string $field_class
+     */
+    public function testReturnsFieldWithNewValue(string $field_class, mixed $value, bool $is_expected_to_set_value): void
     {
-        $form_element_factory           = Mockery::mock(Tracker_FormElementFactory::class);
-        $masschange_data_values_manager = new Tracker_MasschangeDataValueExtractor($form_element_factory);
+        $field = $this->createStub($field_class);
 
-        $text_field_1    = Mockery::mock(Tracker_FormElement_Field_Text::class);
-        $text_field_1_id = 1;
+        $form_element_factory = $this->createStub(Tracker_FormElementFactory::class);
+        $form_element_factory->method('getFieldById')->willReturn($field);
 
-        $text_field_2    = Mockery::mock(Tracker_FormElement_Field_Text::class);
-        $text_field_2_id = 2;
+        $masschange_data = [12 => $value];
 
-        $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);
+        $expected_result = $is_expected_to_set_value ? $masschange_data : [];
 
         $GLOBALS['Language']->method('getText')->willReturn('Unchanged');
 
-        $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_extractor = new Tracker_MasschangeDataValueExtractor($form_element_factory);
 
         $this->assertEquals(
             $expected_result,
-            $masschange_data_values_manager->getNewValues($masschange_data)
+            $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,
+            ],
+        ];
+    }
 }

programming_language: PHP