CVE-2022-31032 (published 2022-06-29T18:15Z)

Tuleap is a Free & Open Source Suite to improve management of software developments and collaboration. In versions prior to 13.9.99.58 authorizations are not properly verified when creating projects or trackers from projects marked as templates. Users can get access to information in those template projects because the permissions model is not properly enforced. Users are advised to upgrade. There are no known workarounds for this issue.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: NVD-CWE-Other
cwe_name: Other
description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/Enalean/tuleap
commit_details: repo_url: https://github.com/Enalean/tuleap
author: Romain LORENTZ
author_date: 2022-05-24 13:56:51+02:00
msg: request #26816 Resources of private projects can be accessed by non project members

Authorizations are not properly verified when creating projects or
trackers from projects marked as templates : A classic user should not
be able create a project from a private template that he is not a
member.

Change-Id: Id8b599432923b32551379041a26d2acf0035a59d
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: ProjectResourceTest.php
change_type: ModificationType.MODIFY
diff: @@ -489,7 +489,7 @@ final class ProjectResourceTest extends \RestBase
         $this->updateArtifactLinks($program_increment['id'], [], $program_increment['artifact_link_id']);
 
         // US1 is linked in top backlog (linked into sprint), US2 is no longer present
-        $this->checkLinksArePresentInReleaseTopBacklog($team_id, [$user_story1['id']]);
+        $this->checkLinksArePresentInReleaseTopBacklog($release_mirror['id'], [$user_story1['id']]);
     }
 
     /**

programming_language: PHP
filename: tuleap-core.po
change_type: ModificationType.MODIFY
diff: @@ -2657,8 +2657,8 @@ msgid "Project"
 msgstr "Projet"
 
 #, php-format
-msgid "Project #%d is not active and is not a template"
-msgstr "Le project #%d n'est pas actif et n'est pas un modèle"
+msgid "Project #%d is not active."
+msgstr "Le project #%d n'est pas actif."
 
 msgid "Project (read only)"
 msgstr "Projet (lecture seule)"
@@ -3476,6 +3476,14 @@ msgstr "Tabulation"
 msgid "Template #%d is not valid"
 msgstr "Le modèle #%d n'est pas valide"
 
+#, php-format
+msgid ""
+"Template %d is not valid: you don't have the permission to the access the "
+"project."
+msgstr ""
+"Le modèle %d n'est pas valide : vous n'avez pas la permission d'accéder au "
+"projet."
+
 msgid "Template used by project"
 msgstr "Modèle utilisé par ce projet"
 

programming_language: CoffeeScript
filename: tuleap-core.po
change_type: ModificationType.MODIFY
diff: @@ -2593,8 +2593,8 @@ msgid "Project"
 msgstr "Projeto"
 
 #, php-format
-msgid "Project #%d is not active and is not a template"
-msgstr "O projeto #%d não está ativo e não é um modelo"
+msgid "Project #%d is not active."
+msgstr "O projeto #%d não está ativo."
 
 msgid "Project (read only)"
 msgstr "Projeto (somente leitura)"
@@ -3391,6 +3391,12 @@ msgstr ""
 msgid "Template #%d is not valid"
 msgstr "O modelo #%d não é válido"
 
+#, php-format
+msgid ""
+"Template %d is not valid: you don't have the permission to the access the "
+"project."
+msgstr ""
+
 msgid "Template used by project"
 msgstr "Modelo utilizado pelo projeto"
 

programming_language: Python
filename: ProjectCreationDataPOSTProjectBuilder.php
change_type: ModificationType.MODIFY
diff: @@ -37,6 +37,7 @@ use Tuleap\Project\Registration\Template\ProjectTemplateIDInvalidException;
 use Tuleap\Project\Registration\Template\TemplateFactory;
 use Tuleap\Project\Registration\Template\TemplateFromProjectForCreation;
 use Tuleap\Project\XML\XMLFileContentRetriever;
+use URLVerification;
 use XML_RNGValidator;
 
 class ProjectCreationDataPOSTProjectBuilder
@@ -55,6 +56,7 @@ class ProjectCreationDataPOSTProjectBuilder
         ServiceManager $service_manager,
         ProjectCreationDataServiceFromXmlInheritor $from_xml_inheritor,
         LoggerInterface $logger,
+        private URLVerification $url_verification,
     ) {
         $this->project_manager            = $project_manager;
         $this->template_factory           = $template_factory;
@@ -93,7 +95,8 @@ class ProjectCreationDataPOSTProjectBuilder
                 TemplateFromProjectForCreation::fromRESTRepresentation(
                     $post_representation,
                     $user,
-                    $this->project_manager
+                    $this->project_manager,
+                    $this->url_verification
                 ),
                 $data
             );

programming_language: PHP
filename: ProjectResource.class.php
change_type: ModificationType.MODIFY
diff: @@ -282,7 +282,8 @@ class ProjectResource extends AuthenticatedResource
                 new ProjectCreationDataServiceFromXmlInheritor(
                     ServiceManager::instance(),
                 ),
-                $this->getBackendLogger()
+                $this->getBackendLogger(),
+                $this->getURLVerification()
             );
 
             $creation_data = $creation_data_post_project_builder->buildProjectCreationDataFromPOSTRepresentation(
@@ -603,7 +604,7 @@ class ProjectResource extends AuthenticatedResource
         $project = $this->project_manager->getProject($id);
         $user    = $this->user_manager->getCurrentUser();
 
-        ProjectAuthorization::userCanAccessProject($user, $project, new URLVerification());
+        ProjectAuthorization::userCanAccessProject($user, $project, $this->getURLVerification());
 
         return $project;
     }
@@ -909,7 +910,7 @@ class ProjectResource extends AuthenticatedResource
     {
         $project = $this->project_manager->getProject($project_id);
         $user    = $this->user_manager->getCurrentUser();
-        ProjectAuthorization::userCanAccessProject($user, $project, new URLVerification());
+        ProjectAuthorization::userCanAccessProject($user, $project, $this->getURLVerification());
 
         return true;
     }
@@ -1434,4 +1435,9 @@ class ProjectResource extends AuthenticatedResource
 
         return $project_field_representations;
     }
+
+    private function getURLVerification(): URLVerification
+    {
+        return new URLVerification();
+    }
 }

programming_language: PHP
filename: InsufficientPermissionToUseCompanyTemplateException.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,37 @@
+<?php
+/**
+ * Copyright (c) Enalean, 2022-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/>.
+ */
+
+namespace Tuleap\Project\Registration\Template;
+
+use Project;
+use Project_Creation_Exception;
+
+final class InsufficientPermissionToUseCompanyTemplateException extends Project_Creation_Exception implements InvalidTemplateException
+{
+    public function __construct(private Project $project)
+    {
+        parent::__construct(sprintf(_("Template %d is not valid: you don't have the permission to the access the project."), $this->project->getId()));
+    }
+
+    public function getI18NMessage(): string
+    {
+        return $this->message;
+    }
+}

programming_language: PHP
filename: ProjectTemplateNotActiveException.php
change_type: ModificationType.MODIFY
diff: @@ -31,12 +31,12 @@ final class ProjectTemplateNotActiveException extends Project_Creation_Exception
 
     public function __construct(Project $project)
     {
-        parent::__construct('Project #' . $project->getID() . ' is not active and is not a template');
+        parent::__construct('Project #' . $project->getID() . ' is not active');
         $this->project = $project;
     }
 
     public function getI18NMessage(): string
     {
-        return sprintf(_('Project #%d is not active and is not a template'), $this->project->getID());
+        return sprintf(_('Project #%d is not active.'), $this->project->getID());
     }
 }

programming_language: PHP
filename: TemplateFactory.php
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,8 @@ declare(strict_types=1);
 
 namespace Tuleap\Project\Registration\Template;
 
+use Project;
+use Project_AccessException;
 use ProjectManager;
 use Psr\EventDispatcher\EventDispatcherInterface;
 use Tuleap\Glyph\GlyphFinder;
@@ -31,6 +33,8 @@ use Tuleap\Project\XML\ConsistencyChecker;
 use Tuleap\Project\XML\ServiceEnableForXmlImportRetriever;
 use Tuleap\Project\XML\XMLFileContentRetriever;
 use Tuleap\XML\ProjectXMLMerger;
+use URLVerification;
+use UserManager;
 
 class TemplateFactory
 {
@@ -63,14 +67,16 @@ class TemplateFactory
         TemplateDao $template_dao,
         ProjectManager $project_manager,
         EventDispatcherInterface $event_dispatcher,
+        private UserManager $user_manager,
+        private URLVerification $url_verification,
     ) {
         $this->template_dao    = $template_dao;
         $this->templates       = [
             AgileALMTemplate::NAME => new AgileALMTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
-            ScrumTemplate::NAME => new ScrumTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
-            KanbanTemplate::NAME => new KanbanTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
-            IssuesTemplate::NAME => new IssuesTemplate($glyph_finder, $consistency_checker, $event_dispatcher),
-            EmptyTemplate::NAME => new EmptyTemplate($glyph_finder),
+            ScrumTemplate::NAME    => new ScrumTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
+            KanbanTemplate::NAME   => new KanbanTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
+            IssuesTemplate::NAME   => new IssuesTemplate($glyph_finder, $consistency_checker, $event_dispatcher),
+            EmptyTemplate::NAME    => new EmptyTemplate($glyph_finder),
         ];
         $this->project_manager = $project_manager;
         $this->glyph_finder    = $glyph_finder;
@@ -96,7 +102,9 @@ class TemplateFactory
             ),
             new TemplateDao(),
             \ProjectManager::instance(),
-            $event_manager
+            $event_manager,
+            \UserManager::instance(),
+            new URLVerification()
         );
     }
 
@@ -166,7 +174,7 @@ class TemplateFactory
         $project_templates = $this->project_manager->getSiteTemplates();
 
         foreach ($project_templates as $project_template) {
-            if ((int) $project_template->getGroupId() === \Project::ADMIN_PROJECT_ID) {
+            if ((int) $project_template->getGroupId() === \Project::ADMIN_PROJECT_ID || ! $this->userCanAccessTemplate($project_template)) {
                 continue;
             }
             $company_templates[] = new CompanyTemplate($project_template, $this->glyph_finder);
@@ -198,4 +206,13 @@ class TemplateFactory
 
         return $templates_by_name;
     }
+
+    private function userCanAccessTemplate(Project $project_template): bool
+    {
+        try {
+            return $this->url_verification->userCanAccessProject($this->user_manager->getCurrentUser(), $project_template);
+        } catch (Project_AccessException $exception) {
+            return false;
+        }
+    }
 }

programming_language: PHP
filename: TemplateFromProjectForCreation.php
change_type: ModificationType.MODIFY
diff: @@ -22,10 +22,12 @@ declare(strict_types=1);
 
 namespace Tuleap\Project\Registration\Template;
 
+use Exception;
 use PFUser;
 use Project;
 use ProjectManager;
 use Tuleap\Project\REST\v1\ProjectPostRepresentation;
+use URLVerification;
 
 class TemplateFromProjectForCreation
 {
@@ -45,9 +47,14 @@ class TemplateFromProjectForCreation
      * @throws ProjectIDTemplateNotProvidedException
      * @throws ProjectTemplateIDInvalidException
      * @throws ProjectTemplateNotActiveException
+     * @throws InsufficientPermissionToUseCompanyTemplateException
      */
-    private static function fromData(ProjectManager $project_manager, PFUser $user_requesting_creation, ?int $project_id): self
-    {
+    private static function fromData(
+        ProjectManager $project_manager,
+        PFUser $user_requesting_creation,
+        ?int $project_id,
+        URLVerification $url_verification,
+    ): self {
         if ($project_id === null) {
             throw new ProjectIDTemplateNotProvidedException();
         }
@@ -58,13 +65,11 @@ class TemplateFromProjectForCreation
             throw new ProjectTemplateIDInvalidException($project_id);
         }
 
-        if (! $project->isActive() && ! $project->isTemplate()) {
+        if (! self::doesProjectStatusAllowUsageAsTemplate($project)) {
             throw new ProjectTemplateNotActiveException($project);
         }
 
-        if (! $project->isTemplate() && ! $user_requesting_creation->isAdmin($project->getID())) {
-            throw new InsufficientPermissionToUseProjectAsTemplateException($project, $user_requesting_creation);
-        }
+        self::checkIfUserHasPermissionsToUseTemplate($project, $url_verification, $user_requesting_creation);
 
         return new self($project);
     }
@@ -79,8 +84,14 @@ class TemplateFromProjectForCreation
         ProjectPostRepresentation $representation,
         PFUser $user_requesting_creation,
         ProjectManager $project_manager,
+        URLVerification $url_verification,
     ): self {
-        return self::fromData($project_manager, $user_requesting_creation, $representation->template_id);
+        return self::fromData(
+            $project_manager,
+            $user_requesting_creation,
+            $representation->template_id,
+            $url_verification
+        );
     }
 
     public static function fromGlobalProjectAdminTemplate(): self
@@ -88,6 +99,30 @@ class TemplateFromProjectForCreation
         return new self(new Project(['group_id' => Project::ADMIN_PROJECT_ID, 'status' => Project::STATUS_SYSTEM]));
     }
 
+    private static function doesProjectStatusAllowUsageAsTemplate(Project $project): bool
+    {
+        return $project->isActive() || $project->isSystem();
+    }
+
+    /**
+     * @throws InsufficientPermissionToUseCompanyTemplateException
+     * @throws InsufficientPermissionToUseProjectAsTemplateException
+     */
+    private static function checkIfUserHasPermissionsToUseTemplate(
+        Project $project,
+        URLVerification $url_verification,
+        PFUser $user_requesting_creation,
+    ): void {
+        if ($project->isTemplate()) {
+            try {
+                $url_verification->userCanAccessProject($user_requesting_creation, $project);
+            } catch (Exception $exception) {
+                throw new InsufficientPermissionToUseCompanyTemplateException($project);
+            }
+        } elseif (! $user_requesting_creation->isAdmin($project->getID())) {
+            throw new InsufficientPermissionToUseProjectAsTemplateException($project, $user_requesting_creation);
+        }
+    }
 
     public function getProject(): Project
     {

programming_language: PHP
filename: TestDataBuilder.php
change_type: ModificationType.MODIFY
diff: @@ -72,6 +72,8 @@ class TestDataBuilder // phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNames
     public const PROJECT_SERVICES                         = 'project-services';
     public const PROJECT_PUBLIC_WITH_MEMBERSHIP_SHORTNAME = 'public-sync-project-member';
     public const PROJECT_FUTURE_RELEASES                  = 'current-future-releases';
+    public const PROJECT_PUBLIC_TEMPLATE                  = 'public-template';
+    public const PROJECT_PRIVATE_TEMPLATE                 = 'private-template';
 
     public const STATIC_UGROUP_1_ID    = 101;
     public const STATIC_UGROUP_1_LABEL = 'static_ugroup_1';

programming_language: PHP
filename: project.xml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+        unix-name="public-template" full-name="Public Template" description="For template test" access="public">
+    <long-description>A public template test project</long-description>
+    <services>
+    </services>
+</project>

programming_language: Markdown
filename: user_map.csv
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+name,action,comment

programming_language: SQL
filename: users.xml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<users>
+</users>

programming_language: SQL
filename: project.xml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+        unix-name="private-template" full-name="Private Template" description="For template test" access="private">
+    <long-description>A private template test project</long-description>
+    <ugroups>
+        <ugroup name="project_members" description="">
+            <members>
+                <member format="username">rest_api_tester_3</member>
+                <member format="username">rest_api_tester_5</member>
+            </members>
+        </ugroup>
+        <ugroup name="project_admins" description="">
+            <members>
+                <member format="username">rest_api_tester_3</member>
+            </members>
+        </ugroup>
+    </ugroups>
+    <services>
+    </services>
+</project>

programming_language: Markdown
filename: user_map.csv
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+name,action,comment

programming_language: SQL
filename: users.xml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<users>
+    <user>
+        <id>104</id>
+        <username>rest_api_tester_3</username>
+        <realname><![CDATA[]]></realname>
+        <email><![CDATA[test_user_3@example.com]]></email>
+        <ldapid></ldapid>
+    </user>
+    <user>
+        <id>105</id>
+        <username>rest_api_tester_5</username>
+        <realname><![CDATA[]]></realname>
+        <email><![CDATA[test_user_5@example.com]]></email>
+        <ldapid></ldapid>
+    </user>
+</users>

programming_language: SQL
filename: init_data.php
change_type: ModificationType.MODIFY
diff: @@ -29,5 +29,6 @@ $data_builder
     ->delegateForgePermissions()
     ->deleteTracker()
     ->deleteProject()
+    ->markProjectsAsTemplate()
     ->suspendProject()
     ->createProjectField();

programming_language: PHP
filename: setup.sh
change_type: ModificationType.MODIFY
diff: @@ -123,6 +123,8 @@ seed_data() {
     load_project /usr/share/tuleap/tests/rest/_fixtures/13-project-services
     load_project /usr/share/tuleap/tests/rest/_fixtures/14-public-sync-project-member
     load_project /usr/share/tuleap/tests/rest/_fixtures/15-future-releases
+    load_project /usr/share/tuleap/tests/rest/_fixtures/16-public-template
+    load_project /usr/share/tuleap/tests/rest/_fixtures/17-private-template
 
     echo "Load initial data"
     PHP="$PHP_CLI" "$PHP_CLI" /usr/share/tuleap/tests/rest/bin/init_data.php

programming_language: Shell
filename: RestBase.php
change_type: ModificationType.MODIFY
diff: @@ -45,6 +45,8 @@ class RestBase extends \Tuleap\Test\PHPUnit\TestCase // phpcs:ignore PSR1.Classe
     protected $project_public_with_membership_id;
     protected $project_future_releases_id;
     protected $project_services_id;
+    protected $project_public_template_id;
+    protected $project_private_template_id;
 
     protected $epic_tracker_id;
     protected $releases_tracker_id;
@@ -121,16 +123,26 @@ class RestBase extends \Tuleap\Test\PHPUnit\TestCase // phpcs:ignore PSR1.Classe
             $this->initUserGroupsId();
         }
 
-        $this->project_private_member_id         = $this->getProjectId(REST_TestDataBuilder::PROJECT_PRIVATE_MEMBER_SHORTNAME);
+        $this->project_private_member_id         = $this->getProjectId(
+            REST_TestDataBuilder::PROJECT_PRIVATE_MEMBER_SHORTNAME
+        );
         $this->project_private_id                = $this->getProjectId(REST_TestDataBuilder::PROJECT_PRIVATE_SHORTNAME);
         $this->project_public_id                 = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_SHORTNAME);
-        $this->project_public_member_id          = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_MEMBER_SHORTNAME);
+        $this->project_public_member_id          = $this->getProjectId(
+            REST_TestDataBuilder::PROJECT_PUBLIC_MEMBER_SHORTNAME
+        );
         $this->project_pbi_id                    = $this->getProjectId(REST_TestDataBuilder::PROJECT_PBI_SHORTNAME);
         $this->project_deleted_id                = $this->getProjectId(REST_TestDataBuilder::PROJECT_DELETED_SHORTNAME);
-        $this->project_suspended_id              = $this->getProjectId(REST_TestDataBuilder::PROJECT_SUSPENDED_SHORTNAME);
-        $this->project_public_with_membership_id = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_WITH_MEMBERSHIP_SHORTNAME);
+        $this->project_suspended_id              = $this->getProjectId(
+            REST_TestDataBuilder::PROJECT_SUSPENDED_SHORTNAME
+        );
+        $this->project_public_with_membership_id = $this->getProjectId(
+            REST_TestDataBuilder::PROJECT_PUBLIC_WITH_MEMBERSHIP_SHORTNAME
+        );
         $this->project_future_releases_id        = $this->getProjectId(REST_TestDataBuilder::PROJECT_FUTURE_RELEASES);
         $this->project_services_id               = $this->getProjectId(REST_TestDataBuilder::PROJECT_SERVICES);
+        $this->project_public_template_id        = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_TEMPLATE);
+        $this->project_private_template_id       = $this->getProjectId(REST_TestDataBuilder::PROJECT_PRIVATE_TEMPLATE);
 
         $this->getTrackerIdsForProjectPrivateMember();
     }

programming_language: PHP
filename: TestDataBuilder.php
change_type: ModificationType.MODIFY
diff: @@ -20,6 +20,7 @@
  */
 
 use Tuleap\admin\ProjectCreation\ProjectFields\ProjectFieldsDao;
+use Tuleap\admin\ProjectEdit\ProjectEditDao;
 use Tuleap\Project\Admin\ProjectDetails\ProjectDetailsDAO;
 use Tuleap\User\ForgeUserGroupPermission\RestProjectManagementPermission;
 use Tuleap\User\ForgeUserGroupPermission\RESTReadOnlyAdmin\RestReadOnlyAdminPermission;
@@ -220,6 +221,26 @@ class REST_TestDataBuilder extends TestDataBuilder  // @codingStandardsIgnoreLin
         return $this;
     }
 
+    public function markProjectsAsTemplate()
+    {
+        echo "Mark public-template and private-template as template";
+
+        $project_manager  = ProjectManager::instance();
+        $public_template  = $project_manager->getProjectByUnixName("public-template");
+        $private_template = $project_manager->getProjectByUnixName("private-template");
+
+        $project_edit_dao = new ProjectEditDao();
+        if ($public_template) {
+            $project_edit_dao->updateProjectStatusAndType("A", TemplateSingleton::TEMPLATE, $public_template->getID());
+        }
+
+        if ($private_template) {
+            $project_edit_dao->updateProjectStatusAndType("A", TemplateSingleton::TEMPLATE, $private_template->getID());
+        }
+
+        return $this;
+    }
+
     public function createProjectField()
     {
         $project_fields_dao  = new ProjectFieldsDao();

programming_language: PHP
filename: ProjectTest.php
change_type: ModificationType.MODIFY
diff: @@ -56,11 +56,11 @@ class ProjectTest extends ProjectBase
             'shortname'  => 'test9747-regular-user',
             'description' => 'Test of Request 9747 for REST API Project Creation',
             'is_public' => true,
-            'template_id' => 100,
+            'template_id' => $this->project_public_template_id,
         ]);
 
         $response = $this->getResponseByName(
-            REST_TestDataBuilder::TEST_USER_2_NAME,
+            REST_TestDataBuilder::TEST_USER_5_NAME,
             $this->request_factory->createRequest(
                 'POST',
                 'projects?dry_run=true'
@@ -86,7 +86,7 @@ class ProjectTest extends ProjectBase
             'shortname'  => 'short_name',
             'description' => '',
             'is_public' => true,
-            'template_id' => 100,
+            'template_id' => $this->project_public_template_id,
             'categories' => [
                 [
                     'category_id' => 15,
@@ -102,7 +102,7 @@ class ProjectTest extends ProjectBase
         ]);
 
         $response = $this->getResponseByName(
-            REST_TestDataBuilder::TEST_USER_2_NAME,
+            REST_TestDataBuilder::TEST_USER_5_NAME,
             $this->request_factory->createRequest(
                 'POST',
                 'projects?dry_run=true'
@@ -159,11 +159,11 @@ class ProjectTest extends ProjectBase
             'shortname'  => 'test9747-regular-user',
             'description' => 'Test of Request 9747 for REST API Project Creation',
             'is_public' => true,
-            'template_id' => 100,
+            'template_id' => $this->project_public_template_id,
         ]);
 
         $response = $this->getResponseByName(
-            REST_TestDataBuilder::TEST_USER_2_NAME,
+            REST_TestDataBuilder::TEST_USER_5_NAME,
             $this->request_factory->createRequest(
                 'POST',
                 'projects?dry_run=true'
@@ -182,11 +182,11 @@ class ProjectTest extends ProjectBase
             'shortname'  => 'test9747-regular-user',
             'description' => 'Test of Request 9747 for REST API Project Creation',
             'is_public' => true,
-            'template_id' => 100,
+            'template_id' => $this->project_public_template_id,
         ]);
 
         $response = $this->getResponseByName(
-            REST_TestDataBuilder::TEST_USER_2_NAME,
+            REST_TestDataBuilder::TEST_USER_5_NAME,
             $this->request_factory->createRequest(
                 'POST',
                 'projects'
@@ -201,8 +201,54 @@ class ProjectTest extends ProjectBase
 
         $this->removeAdminFromProjectMembers(
             $create_project_id,
+            REST_TestDataBuilder::TEST_USER_5_NAME,
+        );
+    }
+
+    public function testPOSTForRegularUserWithTemplateProjectUserCantAccess()
+    {
+        $post_resource = json_encode([
+                                         'label'       => 'Test from template without access',
+                                         'shortname'   => 'template-no-access-user',
+                                         'description' => 'Test project template for REST API Project Creation',
+                                         'is_public'   => true,
+                                         'template_id' => $this->project_private_template_id,
+                                     ]);
+
+        $response = $this->getResponseByName(
             REST_TestDataBuilder::TEST_USER_2_NAME,
+            $this->request_factory->createRequest(
+                'POST',
+                'projects'
+            )->withBody(
+                $this->stream_factory->createStream($post_resource)
+            )
+        );
+
+        self::assertEquals(400, $response->getStatusCode());
+    }
+
+    public function testPOSTForRegularUserWithPrivateTemplateProjectUserCanAccess()
+    {
+        $post_resource = json_encode([
+                                         'label'       => 'Test from private template with access',
+                                         'shortname'   => 'template-private-user-access',
+                                         'description' => 'Test project template for REST API Project Creation',
+                                         'is_public'   => true,
+                                         'template_id' => $this->project_private_template_id,
+                                     ]);
+
+        $response = $this->getResponseByName(
+            REST_TestDataBuilder::TEST_USER_5_NAME,
+            $this->request_factory->createRequest(
+                'POST',
+                'projects'
+            )->withBody(
+                $this->stream_factory->createStream($post_resource)
+            )
         );
+
+        self::assertEquals(201, $response->getStatusCode());
     }
 
     /**
@@ -350,7 +396,7 @@ class ProjectTest extends ProjectBase
             'shortname'  => 'test9748',
             'description' => 'Test of Request 9748 for REST API Project Creation',
             'is_public' => true,
-            'template_id' => 100,
+            'template_id' => $this->project_public_template_id,
         ]);
 
         $response = $this->getResponseByName(

programming_language: PHP
filename: ProjectTest.php
change_type: ModificationType.MODIFY
diff: @@ -40,7 +40,7 @@ class ProjectTest extends ProjectBase
             'shortname'  => 'test9747-ro',
             'description' => 'Test of Request 9747 for REST API Project Creation',
             'is_public' => true,
-            'template_id' => 100,
+            'template_id' => $this->project_public_template_id,
         ]);
 
         $response = $this->getResponse(

programming_language: PHP
filename: ProjectCreationDataPOSTProjectBuilderTest.php
change_type: ModificationType.MODIFY
diff: @@ -75,7 +75,8 @@ final class ProjectCreationDataPOSTProjectBuilderTest extends TestCase
             $this->xml_file_content_retriever,
             $this->service_manager,
             $this->from_xml_inheritor,
-            new NullLogger()
+            new NullLogger(),
+            new \URLVerification()
         );
     }
 

programming_language: PHP
filename: RestProjectCreatorTest.php
change_type: ModificationType.MODIFY
diff: @@ -40,7 +40,6 @@ use Tuleap\ForgeConfigSandbox;
 use Tuleap\Glyph\GlyphFinder;
 use Tuleap\Project\DefaultProjectVisibilityRetriever;
 use Tuleap\Project\Registration\MaxNumberOfProjectReachedForPlatformException;
-use Tuleap\Project\Registration\Template\Events\CollectCategorisedExternalTemplatesEvent;
 use Tuleap\Project\Registration\Template\ScrumTemplate;
 use Tuleap\Project\Registration\Template\TemplateDao;
 use Tuleap\Project\Registration\Template\TemplateFactory;
@@ -51,6 +50,8 @@ use Tuleap\Project\XML\Import\ImportConfig;
 use Tuleap\Project\XML\ServiceEnableForXmlImportRetriever;
 use Tuleap\Project\XML\XMLFileContentRetriever;
 use Tuleap\XML\ProjectXMLMerger;
+use URLVerification;
+use UserManager;
 
 final class RestProjectCreatorTest extends \Tuleap\Test\PHPUnit\TestCase
 {
@@ -95,7 +96,11 @@ final class RestProjectCreatorTest extends \Tuleap\Test\PHPUnit\TestCase
         $this->event_manager  = \Mockery::mock(\EventManager::class);
         $this->retriever      = \Mockery::mock(ServiceEnableForXmlImportRetriever::class);
         $this->plugin_factory = $this->createMock(\PluginFactory::class);
-        $this->creator        = new RestProjectCreator(
+
+        $this->user_manager     = $this->createMock(UserManager::class);
+        $this->url_verification = $this->createMock(URLVerification::class);
+
+        $this->creator = new RestProjectCreator(
             $this->project_creator,
             $this->project_XML_importer,
             new TemplateFactory(
@@ -112,7 +117,8 @@ final class RestProjectCreatorTest extends \Tuleap\Test\PHPUnit\TestCase
                 $this->template_dao,
                 M::mock(ProjectManager::class),
                 new \EventManager(),
-                new CollectCategorisedExternalTemplatesEvent()
+                $this->user_manager,
+                $this->url_verification
             )
         );
 

programming_language: PHP
filename: TemplateFactoryTest.php
change_type: ModificationType.MODIFY
diff: @@ -24,10 +24,15 @@ declare(strict_types=1);
 namespace Tuleap\Project\Registration\Template;
 
 use org\bovigo\vfs\vfsStream;
+use PHPUnit\Framework\MockObject\MockObject;
+use Project_AccessPrivateException;
 use Tuleap\ForgeConfigSandbox;
 use Tuleap\Glyph\GlyphFinder;
 use Tuleap\Project\XML\ConsistencyChecker;
+use Tuleap\Test\Builders\UserTestBuilder;
 use Tuleap\XML\ProjectXMLMerger;
+use URLVerification;
+use UserManager;
 
 final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
 {
@@ -35,20 +40,34 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
 
     private TemplateFactory $factory;
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject&ConsistencyChecker
+     * @var MockObject&ConsistencyChecker
      */
     private $consistency_checker;
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject&\ProjectManager
+     * @var MockObject&\ProjectManager
      */
     private $project_manager;
 
+    /**
+     * @var UserManager&MockObject
+     */
+    private $user_manager;
+    /**
+     * @var URLVerification&MockObject
+     */
+    private $url_verification;
+    private \PFUser $user;
+
     protected function setUp(): void
     {
         \ForgeConfig::set('codendi_cache_dir', vfsStream::setup('TemplateFactoryTest')->url());
 
+        $this->user = UserTestBuilder::aUser()->build();
+
         $this->consistency_checker = $this->createMock(ConsistencyChecker::class);
         $this->project_manager     = $this->createMock(\ProjectManager::class);
+        $this->user_manager        = $this->createMock(UserManager::class);
+        $this->url_verification    = $this->createMock(URLVerification::class);
 
         $this->factory = new TemplateFactory(
             new GlyphFinder(new \EventManager()),
@@ -57,6 +76,8 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
             $this->createMock(TemplateDao::class),
             $this->project_manager,
             new \EventManager(),
+            $this->user_manager,
+            $this->url_verification
         );
     }
 
@@ -130,7 +151,7 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
         $this->factory->getTemplate(ScrumTemplate::NAME);
     }
 
-    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100(): void
+    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100AndUserCanAccess(): void
     {
         $this->consistency_checker->method('areAllServicesAvailable')->willReturn(true);
 
@@ -152,6 +173,9 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
         $template120->method('getDescription')->willReturn("All about us");
         $template120->method('getPublicName')->willReturn("Lyudi Invalidy Company");
 
+        $this->user_manager->method('getCurrentUser')->willReturn($this->user);
+        $this->url_verification->method("userCanAccessProject")->with($this->user, $template100)->willReturn(true);
+
         $site_templates = [$template100, $template110, $template120];
         $this->project_manager->method('getSiteTemplates')->willReturn($site_templates);
 
@@ -163,4 +187,22 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
 
         self::assertEquals($expected_company_templates, $this->factory->getCompanyTemplateList());
     }
+
+    public function testItDoesNotReturnsCompanyTemplateWhenTheUserCannotAccessIt(): void
+    {
+        $this->consistency_checker->method('areAllServicesAvailable')->willReturn(true);
+
+        $template110 = $this->createMock(\Project::class);
+        $template110->expects(self::once())->method('getGroupId')->willReturn("110");
+        $template110->method('getUnixNameLowerCase')->willReturn("hustler-company");
+        $template110->method('getDescription')->willReturn("New Jack City");
+        $template110->method('getPublicName')->willReturn("Hustler Company");
+
+        $this->project_manager->method('getSiteTemplates')->willReturn([$template110]);
+
+        $this->user_manager->method('getCurrentUser')->willReturn($this->user);
+        $this->url_verification->method("userCanAccessProject")->with($this->user, $template110)->willThrowException(new Project_AccessPrivateException());
+
+        self::assertEmpty($this->factory->getCompanyTemplateList());
+    }
 }

programming_language: PHP
filename: TemplateFromProjectForCreationTest.php
change_type: ModificationType.MODIFY
diff: @@ -23,10 +23,12 @@ declare(strict_types=1);
 namespace Tuleap\Project\Registration\Template;
 
 use PFUser;
+use PHPUnit\Framework\MockObject\Stub;
 use Project;
 use ProjectManager;
 use Tuleap\Project\REST\v1\ProjectPostRepresentation;
 use Tuleap\Test\PHPUnit\TestCase;
+use URLVerification;
 
 final class TemplateFromProjectForCreationTest extends TestCase
 {
@@ -38,11 +40,16 @@ final class TemplateFromProjectForCreationTest extends TestCase
      * @var PFUser&\PHPUnit\Framework\MockObject\MockObject
      */
     private $user;
+    /**
+     * @var URLVerification&Stub
+     */
+    private $url_verification;
 
     protected function setUp(): void
     {
-        $this->project_manager = $this->createMock(ProjectManager::class);
-        $this->user            = $this->createMock(PFUser::class);
+        $this->project_manager  = $this->createMock(ProjectManager::class);
+        $this->user             = $this->createMock(PFUser::class);
+        $this->url_verification = $this->createStub(URLVerification::class);
     }
 
     public function testGetTemplateFromProjectForCreationFromRESTRepresentation(): void
@@ -54,7 +61,8 @@ final class TemplateFromProjectForCreationTest extends TestCase
         $template_from_project_for_creation = TemplateFromProjectForCreation::fromRESTRepresentation(
             $representation,
             $this->user,
-            $this->project_manager
+            $this->project_manager,
+            $this->url_verification
         );
 
         self::assertEquals($expected_project, $template_from_project_for_creation->getProject());
@@ -65,9 +73,11 @@ final class TemplateFromProjectForCreationTest extends TestCase
         $project = $this->createMock(Project::class);
         $project->method('getID')->willReturn($project_id);
         $project->method('isError')->willReturn(false);
-        $project->method('isActive')->willReturn(false);
+        $project->method('isActive')->willReturn(true);
         $project->method('isTemplate')->willReturn(true);
 
+        $this->url_verification->method('userCanAccessProject')->willReturn(true);
+
         $this->project_manager->method('getProject')->with($project_id)->willReturn($project);
 
         return $project;
@@ -92,11 +102,12 @@ final class TemplateFromProjectForCreationTest extends TestCase
         TemplateFromProjectForCreation::fromRESTRepresentation(
             $representation,
             $this->user,
-            $this->project_manager
+            $this->project_manager,
+            $this->url_verification
         );
     }
 
-    public function testGetTemplateFromProjectForCreationIsNotValidWhenProjectToUseAsTemplateIsNotActiveAndNotMarkedAsTemplate(): void
+    public function testGetTemplateFromProjectForCreationIsNotValidWhenProjectToUseAsTemplateIsNotActive(): void
     {
         $representation = ProjectPostRepresentation::build(124);
 
@@ -104,7 +115,7 @@ final class TemplateFromProjectForCreationTest extends TestCase
         $project->method('getID')->willReturn($representation->template_id);
         $project->method('isError')->willReturn(false);
         $project->method('isActive')->willReturn(false);
-        $project->method('isTemplate')->willReturn(false);
+        $project->method('isSystem')->willReturn(false);
 
         $this->project_manager->method('getProject')->with($representation->template_id)->willReturn($project);
 
@@ -112,7 +123,8 @@ final class TemplateFromProjectForCreationTest extends TestCase
         TemplateFromProjectForCreation::fromRESTRepresentation(
             $representation,
             $this->user,
-            $this->project_manager
+            $this->project_manager,
+            $this->url_verification
         );
     }
 
@@ -134,7 +146,31 @@ final class TemplateFromProjectForCreationTest extends TestCase
         TemplateFromProjectForCreation::fromRESTRepresentation(
             $representation,
             $this->user,
-            $this->project_manager
+            $this->project_manager,
+            $this->url_verification
+        );
+    }
+
+    public function testGetTemplateFromProjectForCreationIsNotValidWhenTemplateProjectToUseIsActiveButTheUserCanAccessTheProject(): void
+    {
+        $representation = ProjectPostRepresentation::build(125);
+
+        $project = $this->createMock(Project::class);
+        $project->method('getID')->willReturn($representation->template_id);
+        $project->method('isError')->willReturn(false);
+        $project->method('isActive')->willReturn(true);
+        $project->method('isTemplate')->willReturn(true);
+
+        $this->url_verification->method('userCanAccessProject')->willReturn(false);
+
+        $this->project_manager->method('getProject')->with($representation->template_id)->willThrowException(new InsufficientPermissionToUseCompanyTemplateException($project));
+
+        $this->expectException(InsufficientPermissionToUseCompanyTemplateException::class);
+        TemplateFromProjectForCreation::fromRESTRepresentation(
+            $representation,
+            $this->user,
+            $this->project_manager,
+            $this->url_verification
         );
     }
 }

programming_language: PHP
repo_url: https://github.com/Enalean/tuleap
commit_details: repo_url: https://github.com/Enalean/tuleap
author: Nicolas Terray
author_date: 2022-06-06 14:20:26+02:00
msg: Merge commit 'refs/changes/79/25979/8' of ssh://gerrit.tuleap.net:29418/tuleap into HEAD

* ssh://gerrit.tuleap.net:29418/tuleap:
  request #26816 Resources of private projects can be accessed by non project members
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