From 543c579a2e3533103606fad2d5e473f750011e12 Mon Sep 17 00:00:00 2001 From: clearml <> Date: Thu, 5 Dec 2024 22:26:57 +0200 Subject: [PATCH] Do not allow creating a project that has a name or part of the path matching the existing public project --- apiserver/bll/project/project_bll.py | 14 +++++++++++++- apiserver/bll/project/sub_projects.py | 17 +++++++++++++++-- apiserver/tests/automated/test_subprojects.py | 9 +++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/apiserver/bll/project/project_bll.py b/apiserver/bll/project/project_bll.py index 4bc3a68..2a6036c 100644 --- a/apiserver/bll/project/project_bll.py +++ b/apiserver/bll/project/project_bll.py @@ -40,7 +40,7 @@ from .sub_projects import ( _ids_with_children, _ids_with_parents, _get_project_depth, - ProjectsChildren, + ProjectsChildren, _get_writable_project_from_name, ) log = config.logger(__file__) @@ -225,6 +225,18 @@ class ProjectBLL: raise errors.bad_request.ProjectPathExceedsMax(max_depth=max_depth) name, location = _validate_project_name(name) + + existing = _get_writable_project_from_name( + company=company, + name=name, + ) + if existing: + raise errors.bad_request.ExpectedUniqueData( + replacement_msg="Project with the same name already exists", + name=name, + company=company, + ) + now = datetime.utcnow() project = Project( id=database.utils.id(), diff --git a/apiserver/bll/project/sub_projects.py b/apiserver/bll/project/sub_projects.py index ca0883d..d7564cf 100644 --- a/apiserver/bll/project/sub_projects.py +++ b/apiserver/bll/project/sub_projects.py @@ -2,6 +2,8 @@ import itertools from datetime import datetime from typing import Tuple, Optional, Sequence, Mapping +from boltons.iterutils import first + from apiserver import database from apiserver.apierrors import errors from apiserver.database.model import EntityVisibility @@ -96,10 +98,21 @@ def _get_writable_project_from_name( """ Return a project from name. If the project not found then return None """ - qs = Project.objects(company=company, name=name) + qs = Project.objects(company__in=[company, ""], name=name) if _only: + if "company" not in _only: + _only = ["company", *_only] qs = qs.only(*_only) - return qs.first() + projects = list(qs) + + if not projects: + return + + project = first(p for p in projects if p.company == company) + if not project: + raise errors.bad_request.PublicProjectExists(name=name) + + return project ProjectsChildren = Mapping[str, Sequence[Project]] diff --git a/apiserver/tests/automated/test_subprojects.py b/apiserver/tests/automated/test_subprojects.py index cb10afa..e5de764 100644 --- a/apiserver/tests/automated/test_subprojects.py +++ b/apiserver/tests/automated/test_subprojects.py @@ -439,6 +439,15 @@ class TestSubProjects(TestService): self.assertEqual(res2.own_tasks, 0) self.assertEqual(res2.own_models, 0) + def test_public_names_clash(self): + # cannot create a project with a name that match public existing project + with self.api.raises(errors.bad_request.PublicProjectExists): + project = self._temp_project(name="ClearML Examples") + + # cannot create a subproject under a public project + with self.api.raises(errors.bad_request.PublicProjectExists): + project = self._temp_project(name="ClearML Examples/my project") + def test_get_all_with_stats(self): project4, _ = self._temp_project_with_tasks(name="project1/project3/project4") project5, _ = self._temp_project_with_tasks(name="project1/project3/project5")