CVE-2022-31032: Other (NVD-CWE-Other)
value: 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.
CWE 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.
CVSS v3 base score: 4.3
Published: 2022-06-29T18:15Z
Repository: https://github.com/Enalean/tuleap
Language: PHP
Files changed: plugins/program_management/tests/rest/v1/ProjectResourceTest.php, site-content/fr_FR/LC_MESSAGES/tuleap-core.po, site-content/pt_BR/LC_MESSAGES/tuleap-core.po, src/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilder.php, src/common/Project/REST/v1/ProjectResource.class.php, src/common/Project/Registration/Template/InsufficientPermissionToUseCompanyTemplateException.php, src/common/Project/Registration/Template/ProjectTemplateNotActiveException.php, src/common/Project/Registration/Template/TemplateFactory.php, src/common/Project/Registration/Template/TemplateFromProjectForCreation.php, tests/lib/TestDataBuilder.php, tests/rest/_fixtures/16-public-template/project.xml, tests/rest/_fixtures/16-public-template/user_map.csv, tests/rest/_fixtures/16-public-template/users.xml, tests/rest/_fixtures/17-private-template/project.xml, tests/rest/_fixtures/17-private-template/user_map.csv, tests/rest/_fixtures/17-private-template/users.xml, tests/rest/bin/init_data.php, tests/rest/bin/setup.sh, tests/rest/lib/RestBase.php, tests/rest/lib/TestDataBuilder.php, tests/rest/tests/ProjectTest.php, tests/rest/tests/ReadOnlyAdministrator/ProjectTest.php, tests/unit/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilderTest.php, tests/unit/common/Project/REST/v1/RestProjectCreatorTest.php, tests/unit/common/Project/Registration/Template/TemplateFactoryTest.php, tests/unit/common/Project/Registration/Template/TemplateFromProjectForCreationTest.php
Commit date: 2022-06-02 11:14:01 +0200
Commit message: 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

Diff: diff --git a/plugins/program_management/tests/rest/v1/ProjectResourceTest.php b/plugins/program_management/tests/rest/v1/ProjectResourceTest.php
index 2d4ce4a89f4d..dec1098ad7d8 100644
--- a/plugins/program_management/tests/rest/v1/ProjectResourceTest.php
+++ b/plugins/program_management/tests/rest/v1/ProjectResourceTest.php
@@ -487,11 +487,11 @@ final class ProjectResourceTest extends \RestBase
 
         // remove feature in program
         $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']]);
     }
 
     /**
      * @depends testGetProgramIncrements
      */
diff --git a/site-content/fr_FR/LC_MESSAGES/tuleap-core.po b/site-content/fr_FR/LC_MESSAGES/tuleap-core.po
index 3347c592cf49..779bd733fce2 100644
--- a/site-content/fr_FR/LC_MESSAGES/tuleap-core.po
+++ b/site-content/fr_FR/LC_MESSAGES/tuleap-core.po
@@ -2655,12 +2655,12 @@ msgstr "Processeurs"
 
 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)"
 
 msgid "Project Data Export"
@@ -3474,10 +3474,18 @@ msgstr "Tabulation"
 
 #, php-format
 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"
 
 msgid "Templates"
 msgstr "Modèles de projet"
diff --git a/site-content/pt_BR/LC_MESSAGES/tuleap-core.po b/site-content/pt_BR/LC_MESSAGES/tuleap-core.po
index 4cce541af631..9daad004d5f4 100644
--- a/site-content/pt_BR/LC_MESSAGES/tuleap-core.po
+++ b/site-content/pt_BR/LC_MESSAGES/tuleap-core.po
@@ -2591,12 +2591,12 @@ msgstr "Processadores"
 
 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)"
 
 msgid "Project Data Export"
@@ -3389,10 +3389,16 @@ msgstr ""
 
 #, php-format
 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"
 
 msgid "Templates"
 msgstr "Modelos"
diff --git a/src/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilder.php b/src/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilder.php
index ddd473879234..7e34e07cc8f6 100644
--- a/src/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilder.php
+++ b/src/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilder.php
@@ -35,10 +35,11 @@ use Tuleap\Project\Registration\Template\InsufficientPermissionToUseProjectAsTem
 use Tuleap\Project\Registration\Template\ProjectIDTemplateNotProvidedException;
 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
 {
     private ProjectManager $project_manager;
@@ -53,10 +54,11 @@ class ProjectCreationDataPOSTProjectBuilder
         TemplateFactory $template_factory,
         XMLFileContentRetriever $xml_file_content_retriever,
         ServiceManager $service_manager,
         ProjectCreationDataServiceFromXmlInheritor $from_xml_inheritor,
         LoggerInterface $logger,
+        private URLVerification $url_verification,
     ) {
         $this->project_manager            = $project_manager;
         $this->template_factory           = $template_factory;
         $this->xml_file_content_retriever = $xml_file_content_retriever;
         $this->service_manager            = $service_manager;
@@ -91,11 +93,12 @@ class ProjectCreationDataPOSTProjectBuilder
             $creation_data = ProjectCreationData::buildFromFormArray(
                 new DefaultProjectVisibilityRetriever(),
                 TemplateFromProjectForCreation::fromRESTRepresentation(
                     $post_representation,
                     $user,
-                    $this->project_manager
+                    $this->project_manager,
+                    $this->url_verification
                 ),
                 $data
             );
 
             $creation_data->setUnixName($post_representation->shortname);
diff --git a/src/common/Project/REST/v1/ProjectResource.class.php b/src/common/Project/REST/v1/ProjectResource.class.php
index 7ef894add7bb..e5b02ccf600a 100644
--- a/src/common/Project/REST/v1/ProjectResource.class.php
+++ b/src/common/Project/REST/v1/ProjectResource.class.php
@@ -280,11 +280,12 @@ class ProjectResource extends AuthenticatedResource
                 new XMLFileContentRetriever(),
                 ServiceManager::instance(),
                 new ProjectCreationDataServiceFromXmlInheritor(
                     ServiceManager::instance(),
                 ),
-                $this->getBackendLogger()
+                $this->getBackendLogger(),
+                $this->getURLVerification()
             );
 
             $creation_data = $creation_data_post_project_builder->buildProjectCreationDataFromPOSTRepresentation(
                 $post_representation,
                 $user
@@ -601,11 +602,11 @@ class ProjectResource extends AuthenticatedResource
     private function getProjectForUser($id)
     {
         $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;
     }
 
     /**
@@ -907,11 +908,11 @@ class ProjectResource extends AuthenticatedResource
      */
     private function userCanSeeUserGroups($project_id)
     {
         $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;
     }
 
     /**
@@ -1432,6 +1433,11 @@ class ProjectResource extends AuthenticatedResource
             $project_field_representations[] = new ProjectFieldsMinimalRepresentation($key, $value);
         }
 
         return $project_field_representations;
     }
+
+    private function getURLVerification(): URLVerification
+    {
+        return new URLVerification();
+    }
 }
diff --git a/src/common/Project/Registration/Template/InsufficientPermissionToUseCompanyTemplateException.php b/src/common/Project/Registration/Template/InsufficientPermissionToUseCompanyTemplateException.php
new file mode 100644
index 000000000000..a1b45d1c1cf3
--- /dev/null
+++ b/src/common/Project/Registration/Template/InsufficientPermissionToUseCompanyTemplateException.php
@@ -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;
+    }
+}
diff --git a/src/common/Project/Registration/Template/ProjectTemplateNotActiveException.php b/src/common/Project/Registration/Template/ProjectTemplateNotActiveException.php
index 683870affeeb..30f73f0ec894 100644
--- a/src/common/Project/Registration/Template/ProjectTemplateNotActiveException.php
+++ b/src/common/Project/Registration/Template/ProjectTemplateNotActiveException.php
@@ -29,14 +29,14 @@ final class ProjectTemplateNotActiveException extends Project_Creation_Exception
 {
     private Project $project;
 
     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());
     }
 }
diff --git a/src/common/Project/Registration/Template/TemplateFactory.php b/src/common/Project/Registration/Template/TemplateFactory.php
index 1d1cca0215fd..cd062038bbb2 100644
--- a/src/common/Project/Registration/Template/TemplateFactory.php
+++ b/src/common/Project/Registration/Template/TemplateFactory.php
@@ -21,18 +21,22 @@
 
 declare(strict_types=1);
 
 namespace Tuleap\Project\Registration\Template;
 
+use Project;
+use Project_AccessException;
 use ProjectManager;
 use Psr\EventDispatcher\EventDispatcherInterface;
 use Tuleap\Glyph\GlyphFinder;
 use Tuleap\Project\Registration\Template\Events\CollectCategorisedExternalTemplatesEvent;
 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
 {
     /**
      * @var array<string,TuleapTemplate>
@@ -61,18 +65,20 @@ class TemplateFactory
         ProjectXMLMerger $project_xml_merger,
         ConsistencyChecker $consistency_checker,
         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;
 
         $this->external_templates = self::getExternalTemplatesByName($event_dispatcher);
@@ -94,11 +100,13 @@ class TemplateFactory
                 new ServiceEnableForXmlImportRetriever($plugin_factory),
                 $plugin_factory,
             ),
             new TemplateDao(),
             \ProjectManager::instance(),
-            $event_manager
+            $event_manager,
+            \UserManager::instance(),
+            new URLVerification()
         );
     }
 
     /**
      * @return ProjectTemplate[]
@@ -164,11 +172,11 @@ class TemplateFactory
     {
         $company_templates = [];
         $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);
         }
         return $company_templates;
@@ -196,6 +204,15 @@ class TemplateFactory
             $templates_by_name[$template->getId()] = $template;
         }
 
         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;
+        }
+    }
 }
diff --git a/src/common/Project/Registration/Template/TemplateFromProjectForCreation.php b/src/common/Project/Registration/Template/TemplateFromProjectForCreation.php
index d5e439f7d77e..7ec4d2690922 100644
--- a/src/common/Project/Registration/Template/TemplateFromProjectForCreation.php
+++ b/src/common/Project/Registration/Template/TemplateFromProjectForCreation.php
@@ -20,14 +20,16 @@
 
 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
 {
     /**
      * @var Project
@@ -43,30 +45,33 @@ class TemplateFromProjectForCreation
     /**
      * @throws InsufficientPermissionToUseProjectAsTemplateException
      * @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();
         }
 
         $project = $project_manager->getProject($project_id);
 
         if ($project->isError()) {
             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);
     }
 
     /**
@@ -77,19 +82,49 @@ class TemplateFromProjectForCreation
      */
     public static function fromRESTRepresentation(
         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
     {
         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
     {
         return $this->project;
     }
diff --git a/tests/lib/TestDataBuilder.php b/tests/lib/TestDataBuilder.php
index 290018264e12..9a5821bdfbe5 100644
--- a/tests/lib/TestDataBuilder.php
+++ b/tests/lib/TestDataBuilder.php
@@ -70,10 +70,12 @@ class TestDataBuilder // phpcs:ignore PSR1.Classes.ClassDeclaration.MissingNames
     public const PROJECT_DELETED_SHORTNAME                = 'deleted-project';
     public const PROJECT_SUSPENDED_SHORTNAME              = 'suspended-project';
     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';
 
     public const STATIC_UGROUP_2_ID    = 102;
diff --git a/tests/rest/_fixtures/16-public-template/project.xml b/tests/rest/_fixtures/16-public-template/project.xml
new file mode 100644
index 000000000000..ae3e12fe7449
--- /dev/null
+++ b/tests/rest/_fixtures/16-public-template/project.xml
@@ -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>
diff --git a/tests/rest/_fixtures/16-public-template/user_map.csv b/tests/rest/_fixtures/16-public-template/user_map.csv
new file mode 100644
index 000000000000..ead57d379498
--- /dev/null
+++ b/tests/rest/_fixtures/16-public-template/user_map.csv
@@ -0,0 +1 @@
+name,action,comment
diff --git a/tests/rest/_fixtures/16-public-template/users.xml b/tests/rest/_fixtures/16-public-template/users.xml
new file mode 100644
index 000000000000..6e009d283309
--- /dev/null
+++ b/tests/rest/_fixtures/16-public-template/users.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<users>
+</users>
diff --git a/tests/rest/_fixtures/17-private-template/project.xml b/tests/rest/_fixtures/17-private-template/project.xml
new file mode 100644
index 000000000000..a9c1f88ee226
--- /dev/null
+++ b/tests/rest/_fixtures/17-private-template/project.xml
@@ -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>
diff --git a/tests/rest/_fixtures/17-private-template/user_map.csv b/tests/rest/_fixtures/17-private-template/user_map.csv
new file mode 100644
index 000000000000..ead57d379498
--- /dev/null
+++ b/tests/rest/_fixtures/17-private-template/user_map.csv
@@ -0,0 +1 @@
+name,action,comment
diff --git a/tests/rest/_fixtures/17-private-template/users.xml b/tests/rest/_fixtures/17-private-template/users.xml
new file mode 100644
index 000000000000..4685679e3db4
--- /dev/null
+++ b/tests/rest/_fixtures/17-private-template/users.xml
@@ -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>
diff --git a/tests/rest/bin/init_data.php b/tests/rest/bin/init_data.php
index 45ac09c444d5..f9de41d5e9e1 100644
--- a/tests/rest/bin/init_data.php
+++ b/tests/rest/bin/init_data.php
@@ -27,7 +27,8 @@ $data_builder
     ->instanciateFactories()
     ->generateUsers()
     ->delegateForgePermissions()
     ->deleteTracker()
     ->deleteProject()
+    ->markProjectsAsTemplate()
     ->suspendProject()
     ->createProjectField();
diff --git a/tests/rest/bin/setup.sh b/tests/rest/bin/setup.sh
index 02ab3420645d..c327d09ba36b 100755
--- a/tests/rest/bin/setup.sh
+++ b/tests/rest/bin/setup.sh
@@ -121,10 +121,12 @@ seed_data() {
     load_project /usr/share/tuleap/tests/rest/_fixtures/11-delegated-rest-project-managers
     load_project /usr/share/tuleap/tests/rest/_fixtures/12-suspended-project
     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
 
     seed_plugin_data
diff --git a/tests/rest/lib/RestBase.php b/tests/rest/lib/RestBase.php
index 1c2221c595f6..d79e7bd41145 100644
--- a/tests/rest/lib/RestBase.php
+++ b/tests/rest/lib/RestBase.php
@@ -43,10 +43,12 @@ class RestBase extends \Tuleap\Test\PHPUnit\TestCase // phpcs:ignore PSR1.Classe
     protected $project_deleted_id;
     protected $project_suspended_id;
     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;
     protected $sprints_tracker_id;
     protected $tasks_tracker_id;
@@ -119,20 +121,30 @@ class RestBase extends \Tuleap\Test\PHPUnit\TestCase // phpcs:ignore PSR1.Classe
         $this->user_groups_ids = $this->cache->getUserGroupIds();
         if (! $this->user_groups_ids) {
             $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();
     }
 
     protected function getResponse($request, $user_name = REST_TestDataBuilder::TEST_USER_1_NAME)
diff --git a/tests/rest/lib/TestDataBuilder.php b/tests/rest/lib/TestDataBuilder.php
index 546ee73fcf50..9a42868d4b55 100644
--- a/tests/rest/lib/TestDataBuilder.php
+++ b/tests/rest/lib/TestDataBuilder.php
@@ -18,10 +18,11 @@
  * along with Tuleap. If not, see <http://www.gnu.org/licenses/>.
  *
  */
 
 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;
 
 require_once __DIR__ . '/../../lib/TestDataBuilder.php';
@@ -218,10 +219,30 @@ class REST_TestDataBuilder extends TestDataBuilder  // @codingStandardsIgnoreLin
         $project_manager->updateStatus($project, PROJECT::STATUS_DELETED);
 
         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();
         $project_details_dao = new ProjectDetailsDAO();
         $project_fields_dao->createProjectField("Test Rest", "Field for test rest", 2, "text", 0);
diff --git a/tests/rest/tests/ProjectTest.php b/tests/rest/tests/ProjectTest.php
index 1929bfb1c894..e0ee553ee41c 100644
--- a/tests/rest/tests/ProjectTest.php
+++ b/tests/rest/tests/ProjectTest.php
@@ -54,15 +54,15 @@ class ProjectTest extends ProjectBase
         $post_resource = json_encode([
             'label' => 'Test Request 9747 regular user',
             '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'
             )->withBody(
                 $this->stream_factory->createStream($post_resource)
@@ -84,11 +84,11 @@ class ProjectTest extends ProjectBase
         $post_resource = json_encode([
             'label' => 'Looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong name',
             'shortname'  => 'short_name',
             'description' => '',
             'is_public' => true,
-            'template_id' => 100,
+            'template_id' => $this->project_public_template_id,
             'categories' => [
                 [
                     'category_id' => 15,
                     'value_id' => 21,
                 ],
@@ -100,11 +100,11 @@ 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'
             )->withBody(
                 $this->stream_factory->createStream($post_resource)
@@ -157,15 +157,15 @@ class ProjectTest extends ProjectBase
         $post_resource = json_encode([
             'label' => 'Test Request 9747 regular user',
             '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'
             )->withBody(
                 $this->stream_factory->createStream($post_resource)
@@ -180,15 +180,15 @@ class ProjectTest extends ProjectBase
         $post_resource = json_encode([
             'label' => 'Test Request 9747 regular user',
             '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'
             )->withBody(
                 $this->stream_factory->createStream($post_resource)
@@ -199,12 +199,58 @@ class ProjectTest extends ProjectBase
 
         $create_project_id = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR)['id'];
 
         $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());
     }
 
     /**
      * Transfer project ownership and administration to a catch all user
      *
@@ -348,11 +394,11 @@ class ProjectTest extends ProjectBase
         $post_resource = json_encode([
             'label' => 'Test Request 9748',
             '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(
             REST_TestDataBuilder::TEST_USER_DELEGATED_REST_PROJECT_MANAGER_NAME,
             $this->request_factory->createRequest(
diff --git a/tests/rest/tests/ReadOnlyAdministrator/ProjectTest.php b/tests/rest/tests/ReadOnlyAdministrator/ProjectTest.php
index 6df829b525ed..aa140bc21650 100644
--- a/tests/rest/tests/ReadOnlyAdministrator/ProjectTest.php
+++ b/tests/rest/tests/ReadOnlyAdministrator/ProjectTest.php
@@ -38,11 +38,11 @@ class ProjectTest extends ProjectBase
         $post_resource = json_encode([
             'label' => 'Test Request 9747 read only',
             '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(
             $this->request_factory->createRequest(
                 'POST',
diff --git a/tests/unit/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilderTest.php b/tests/unit/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilderTest.php
index ae90bc0900ca..a79ddbce6f3d 100644
--- a/tests/unit/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilderTest.php
+++ b/tests/unit/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilderTest.php
@@ -73,11 +73,12 @@ final class ProjectCreationDataPOSTProjectBuilderTest extends TestCase
             $this->project_manager,
             $this->template_factory,
             $this->xml_file_content_retriever,
             $this->service_manager,
             $this->from_xml_inheritor,
-            new NullLogger()
+            new NullLogger(),
+            new \URLVerification()
         );
     }
 
     public function testItBuildsProjectCreationDataWithTemplateId(): void
     {
diff --git a/tests/unit/common/Project/REST/v1/RestProjectCreatorTest.php b/tests/unit/common/Project/REST/v1/RestProjectCreatorTest.php
index 9905f86d6ad3..6a96ed13ee67 100644
--- a/tests/unit/common/Project/REST/v1/RestProjectCreatorTest.php
+++ b/tests/unit/common/Project/REST/v1/RestProjectCreatorTest.php
@@ -38,21 +38,22 @@ use Service;
 use ServiceManager;
 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;
 use Tuleap\Project\SystemEventRunnerForProjectCreationFromXMLTemplate;
 use Tuleap\Project\XML\ConsistencyChecker;
 use Tuleap\Project\XML\Import\ArchiveInterface;
 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
 {
     use M\Adapter\Phpunit\MockeryPHPUnitIntegration;
     use ForgeConfigSandbox;
@@ -93,11 +94,15 @@ final class RestProjectCreatorTest extends \Tuleap\Test\PHPUnit\TestCase
         $this->template_dao         = M::mock(TemplateDao::class);
 
         $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(
                 new GlyphFinder(
                     new \EventManager()
@@ -110,11 +115,12 @@ final class RestProjectCreatorTest extends \Tuleap\Test\PHPUnit\TestCase
                     $this->plugin_factory,
                 ),
                 $this->template_dao,
                 M::mock(ProjectManager::class),
                 new \EventManager(),
-                new CollectCategorisedExternalTemplatesEvent()
+                $this->user_manager,
+                $this->url_verification
             )
         );
 
         $this->user = new \PFUser(['language_id' => 'en_US']);
         $this->project_manager->shouldReceive('userCanCreateProject')->with($this->user)->andReturnTrue()->byDefault();
diff --git a/tests/unit/common/Project/Registration/Template/TemplateFactoryTest.php b/tests/unit/common/Project/Registration/Template/TemplateFactoryTest.php
index 16c004b453f3..17763cfdfa69 100644
--- a/tests/unit/common/Project/Registration/Template/TemplateFactoryTest.php
+++ b/tests/unit/common/Project/Registration/Template/TemplateFactoryTest.php
@@ -22,43 +22,64 @@
 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
 {
     use ForgeConfigSandbox;
 
     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()),
             new ProjectXMLMerger(),
             $this->consistency_checker,
             $this->createMock(TemplateDao::class),
             $this->project_manager,
             new \EventManager(),
+            $this->user_manager,
+            $this->url_verification
         );
     }
 
     public function testItReturnsTemplates(): void
     {
@@ -128,11 +149,11 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
         $this->expectException(InvalidXMLTemplateNameException::class);
 
         $this->factory->getTemplate(ScrumTemplate::NAME);
     }
 
-    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100(): void
+    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100AndUserCanAccess(): void
     {
         $this->consistency_checker->method('areAllServicesAvailable')->willReturn(true);
 
         $template100 = $this->createMock(\Project::class);
         $template100->expects(self::once())->method('getGroupId')->willReturn("100");
@@ -150,10 +171,13 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
         $template120->expects(self::atLeast(2))->method('getGroupId')->willReturn("120");
         $template120->method('getUnixNameLowerCase')->willReturn("lyudi-invalidy-company");
         $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);
 
         $glyph_finder      = new GlyphFinder($this->createMock(\EventManager::class));
         $hustler_template  = new CompanyTemplate($template110, $glyph_finder);
@@ -161,6 +185,24 @@ final class TemplateFactoryTest extends \Tuleap\Test\PHPUnit\TestCase
 
         $expected_company_templates = [$hustler_template, $invalidy_template];
 
         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());
+    }
 }
diff --git a/tests/unit/common/Project/Registration/Template/TemplateFromProjectForCreationTest.php b/tests/unit/common/Project/Registration/Template/TemplateFromProjectForCreationTest.php
index 35f87fa3e710..5b312713041c 100644
--- a/tests/unit/common/Project/Registration/Template/TemplateFromProjectForCreationTest.php
+++ b/tests/unit/common/Project/Registration/Template/TemplateFromProjectForCreationTest.php
@@ -21,14 +21,16 @@
 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
 {
     /**
      * @var \PHPUnit\Framework\MockObject\MockObject&ProjectManager
@@ -36,15 +38,20 @@ final class TemplateFromProjectForCreationTest extends TestCase
     private $project_manager;
     /**
      * @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
     {
         $representation = ProjectPostRepresentation::build(123);
@@ -52,24 +59,27 @@ final class TemplateFromProjectForCreationTest extends TestCase
         $expected_project = $this->mockForSuccessfulValidation($representation->template_id);
 
         $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());
     }
 
     private function mockForSuccessfulValidation(int $project_id): Project
     {
         $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;
     }
 
@@ -90,31 +100,33 @@ final class TemplateFromProjectForCreationTest extends TestCase
 
         $this->expectException(ProjectTemplateIDInvalidException::class);
         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);
 
         $project = $this->createMock(Project::class);
         $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);
 
         $this->expectException(ProjectTemplateNotActiveException::class);
         TemplateFromProjectForCreation::fromRESTRepresentation(
             $representation,
             $this->user,
-            $this->project_manager
+            $this->project_manager,
+            $this->url_verification
         );
     }
 
     public function testGetTemplateFromProjectForCreationIsNotValidWhenProjectToUseIsActiveButTheUserRequestingTheCreationIsNotOneOfTheProjectAdmins(): void
     {
@@ -132,9 +144,33 @@ final class TemplateFromProjectForCreationTest extends TestCase
 
         $this->expectException(InsufficientPermissionToUseProjectAsTemplateException::class);
         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
         );
     }
 }

Vulnerable code:         $this->checkLinksArePresentInReleaseTopBacklog($team_id, [$user_story1['id']]);
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 and is not a template"
msgstr "O projeto #%d não está ativo e não é um modelo"
                    $this->project_manager
                $this->getBackendLogger()
        ProjectAuthorization::userCanAccessProject($user, $project, new URLVerification());
        ProjectAuthorization::userCanAccessProject($user, $project, new URLVerification());
        parent::__construct('Project #' . $project->getID() . ' is not active and is not a template');
        return sprintf(_('Project #%d is not active and is not a template'), $this->project->getID());
            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),
            $event_manager
            if ((int) $project_template->getGroupId() === \Project::ADMIN_PROJECT_ID) {
    private static function fromData(ProjectManager $project_manager, PFUser $user_requesting_creation, ?int $project_id): self
    {
        if (! $project->isActive() && ! $project->isTemplate()) {
        if (! $project->isTemplate() && ! $user_requesting_creation->isAdmin($project->getID())) {
            throw new InsufficientPermissionToUseProjectAsTemplateException($project, $user_requesting_creation);
        }
        return self::fromData($project_manager, $user_requesting_creation, $representation->template_id);
        $this->project_private_member_id         = $this->getProjectId(REST_TestDataBuilder::PROJECT_PRIVATE_MEMBER_SHORTNAME);
        $this->project_public_member_id          = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_MEMBER_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);
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            'template_id' => 100,
            new NullLogger()
use Tuleap\Project\Registration\Template\Events\CollectCategorisedExternalTemplatesEvent;
        $this->creator        = new RestProjectCreator(
                new CollectCategorisedExternalTemplatesEvent()
     * @var \PHPUnit\Framework\MockObject\MockObject&ConsistencyChecker
     * @var \PHPUnit\Framework\MockObject\MockObject&\ProjectManager
    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100(): void
        $this->project_manager = $this->createMock(ProjectManager::class);
        $this->user            = $this->createMock(PFUser::class);
            $this->project_manager
        $project->method('isActive')->willReturn(false);
            $this->project_manager
    public function testGetTemplateFromProjectForCreationIsNotValidWhenProjectToUseAsTemplateIsNotActiveAndNotMarkedAsTemplate(): void
        $project->method('isTemplate')->willReturn(false);
            $this->project_manager
            $this->project_manager
Fixed code:         $this->checkLinksArePresentInReleaseTopBacklog($release_mirror['id'], [$user_story1['id']]);
msgid "Project #%d is not active."
msgstr "Le project #%d n'est pas actif."
#, 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 "Project #%d is not active."
msgstr "O projeto #%d não está ativo."
#, php-format
msgid ""
"Template %d is not valid: you don't have the permission to the access the "
"project."
msgstr ""

use URLVerification;
        private URLVerification $url_verification,
                    $this->project_manager,
                    $this->url_verification
                $this->getBackendLogger(),
                $this->getURLVerification()
        ProjectAuthorization::userCanAccessProject($user, $project, $this->getURLVerification());
        ProjectAuthorization::userCanAccessProject($user, $project, $this->getURLVerification());

    private function getURLVerification(): URLVerification
    {
        return new URLVerification();
    }
<?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;
    }
}
        parent::__construct('Project #' . $project->getID() . ' is not active');
        return sprintf(_('Project #%d is not active.'), $this->project->getID());
use Project;
use Project_AccessException;
use URLVerification;
use UserManager;
        private UserManager $user_manager,
        private URLVerification $url_verification,
            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),
            $event_manager,
            \UserManager::instance(),
            new URLVerification()
            if ((int) $project_template->getGroupId() === \Project::ADMIN_PROJECT_ID || ! $this->userCanAccessTemplate($project_template)) {

    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;
        }
    }
use Exception;
use URLVerification;
     * @throws InsufficientPermissionToUseCompanyTemplateException
    private static function fromData(
        ProjectManager $project_manager,
        PFUser $user_requesting_creation,
        ?int $project_id,
        URLVerification $url_verification,
    ): self {
        if (! self::doesProjectStatusAllowUsageAsTemplate($project)) {
        self::checkIfUserHasPermissionsToUseTemplate($project, $url_verification, $user_requesting_creation);
        URLVerification $url_verification,
        return self::fromData(
            $project_manager,
            $user_requesting_creation,
            $representation->template_id,
            $url_verification
        );
    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 const PROJECT_PUBLIC_TEMPLATE                  = 'public-template';
    public const PROJECT_PRIVATE_TEMPLATE                 = 'private-template';
<?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>
name,action,comment
<?xml version="1.0" encoding="UTF-8"?>
<users>
</users>
<?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>
name,action,comment
<?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>
    ->markProjectsAsTemplate()
    load_project /usr/share/tuleap/tests/rest/_fixtures/16-public-template
    load_project /usr/share/tuleap/tests/rest/_fixtures/17-private-template
    protected $project_public_template_id;
    protected $project_private_template_id;
        $this->project_private_member_id         = $this->getProjectId(
            REST_TestDataBuilder::PROJECT_PRIVATE_MEMBER_SHORTNAME
        );
        $this->project_public_member_id          = $this->getProjectId(
            REST_TestDataBuilder::PROJECT_PUBLIC_MEMBER_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_public_template_id        = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_TEMPLATE);
        $this->project_private_template_id       = $this->getProjectId(REST_TestDataBuilder::PROJECT_PRIVATE_TEMPLATE);
use Tuleap\admin\ProjectEdit\ProjectEditDao;
    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;
    }

            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            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(
            $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());
            'template_id' => $this->project_public_template_id,
            'template_id' => $this->project_public_template_id,
            new NullLogger(),
            new \URLVerification()
use URLVerification;
use UserManager;

        $this->user_manager     = $this->createMock(UserManager::class);
        $this->url_verification = $this->createMock(URLVerification::class);

        $this->creator = new RestProjectCreator(
                $this->user_manager,
                $this->url_verification
use PHPUnit\Framework\MockObject\MockObject;
use Project_AccessPrivateException;
use Tuleap\Test\Builders\UserTestBuilder;
use URLVerification;
use UserManager;
     * @var MockObject&ConsistencyChecker
     * @var MockObject&\ProjectManager
    /**
     * @var UserManager&MockObject
     */
    private $user_manager;
    /**
     * @var URLVerification&MockObject
     */
    private $url_verification;
    private \PFUser $user;

        $this->user = UserTestBuilder::aUser()->build();

        $this->user_manager        = $this->createMock(UserManager::class);
        $this->url_verification    = $this->createMock(URLVerification::class);
            $this->user_manager,
            $this->url_verification
    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100AndUserCanAccess(): void
        $this->user_manager->method('getCurrentUser')->willReturn($this->user);
        $this->url_verification->method("userCanAccessProject")->with($this->user, $template100)->willReturn(true);


    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());
    }
use PHPUnit\Framework\MockObject\Stub;
use URLVerification;
    /**
     * @var URLVerification&Stub
     */
    private $url_verification;
        $this->project_manager  = $this->createMock(ProjectManager::class);
        $this->user             = $this->createMock(PFUser::class);
        $this->url_verification = $this->createStub(URLVerification::class);
            $this->project_manager,
            $this->url_verification
        $project->method('isActive')->willReturn(true);
        $this->url_verification->method('userCanAccessProject')->willReturn(true);

            $this->project_manager,
            $this->url_verification
    public function testGetTemplateFromProjectForCreationIsNotValidWhenProjectToUseAsTemplateIsNotActive(): void
        $project->method('isSystem')->willReturn(false);
            $this->project_manager,
            $this->url_verification
            $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