Skip to content

Commit f272c8f

Browse files
🐛 do not allow moving folder to a child folder (#6370)
1 parent 662030a commit f272c8f

File tree

5 files changed

+81
-6
lines changed

5 files changed

+81
-6
lines changed

services/web/server/src/simcore_service_webserver/folders/_folders_api.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@
1515
from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY
1616
from servicelib.common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
1717
from servicelib.utils import fire_and_forget_task
18-
from simcore_service_webserver.workspaces._workspaces_api import (
19-
check_user_workspace_access,
20-
)
2118

19+
from ..folders.errors import FolderValueNotPermittedError
2220
from ..projects.projects_api import submit_delete_project_task
2321
from ..users.api import get_user
22+
from ..workspaces._workspaces_api import check_user_workspace_access
2423
from ..workspaces.errors import (
2524
WorkspaceAccessForbiddenError,
2625
WorkspaceFolderInconsistencyError,
@@ -238,6 +237,24 @@ async def update_folder(
238237
workspace_id=folder_db.workspace_id,
239238
)
240239

240+
if folder_db.parent_folder_id != parent_folder_id and parent_folder_id is not None:
241+
# Check user has access to the parent folder
242+
await folders_db.get_for_user_or_workspace(
243+
app,
244+
folder_id=parent_folder_id,
245+
product_name=product_name,
246+
user_id=user_id if workspace_is_private else None,
247+
workspace_id=folder_db.workspace_id,
248+
)
249+
# Do not allow to move to a child folder id
250+
_child_folders = await folders_db.get_folders_recursively(
251+
app, folder_id=folder_id, product_name=product_name
252+
)
253+
if parent_folder_id in _child_folders:
254+
raise FolderValueNotPermittedError(
255+
reason="Parent folder id should not be one of children"
256+
)
257+
241258
folder_db = await folders_db.update(
242259
app,
243260
folder_id=folder_id,

services/web/server/src/simcore_service_webserver/folders/_folders_db.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,40 @@ async def get_projects_recursively_only_if_user_is_owner(
322322
rows = await result.fetchall() or []
323323
results = [ProjectID(row[0]) for row in rows]
324324
return results
325+
326+
327+
async def get_folders_recursively(
328+
app: web.Application,
329+
*,
330+
folder_id: FolderID,
331+
product_name: ProductName,
332+
) -> list[FolderID]:
333+
async with get_database_engine(app).acquire() as conn, conn.begin():
334+
# Step 1: Define the base case for the recursive CTE
335+
base_query = select(
336+
folders_v2.c.folder_id, folders_v2.c.parent_folder_id
337+
).where(
338+
(folders_v2.c.folder_id == folder_id) # <-- specified folder id
339+
& (folders_v2.c.product_name == product_name)
340+
)
341+
folder_hierarchy_cte = base_query.cte(name="folder_hierarchy", recursive=True)
342+
# Step 2: Define the recursive case
343+
folder_alias = aliased(folders_v2)
344+
recursive_query = select(
345+
folder_alias.c.folder_id, folder_alias.c.parent_folder_id
346+
).select_from(
347+
folder_alias.join(
348+
folder_hierarchy_cte,
349+
folder_alias.c.parent_folder_id == folder_hierarchy_cte.c.folder_id,
350+
)
351+
)
352+
# Step 3: Combine base and recursive cases into a CTE
353+
folder_hierarchy_cte = folder_hierarchy_cte.union_all(recursive_query)
354+
# Step 4: Execute the query to get all descendants
355+
final_query = select(folder_hierarchy_cte)
356+
result = await conn.execute(final_query)
357+
rows = ( # list of tuples [(folder_id, parent_folder_id), ...] ex. [(1, None), (2, 1)]
358+
await result.fetchall() or []
359+
)
360+
361+
return [FolderID(row[0]) for row in rows]

services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
2929
from servicelib.request_keys import RQT_USERID_KEY
3030
from servicelib.rest_constants import RESPONSE_MODEL_POLICY
31-
from simcore_postgres_database.utils_folders import FoldersError
3231

3332
from .._constants import RQ_PRODUCT_KEY
3433
from .._meta import API_VTAG as VTAG
@@ -41,7 +40,12 @@
4140
WorkspaceNotFoundError,
4241
)
4342
from . import _folders_api
44-
from .errors import FolderAccessForbiddenError, FolderNotFoundError
43+
from .errors import (
44+
FolderAccessForbiddenError,
45+
FolderNotFoundError,
46+
FoldersValueError,
47+
FolderValueNotPermittedError,
48+
)
4549

4650
_logger = logging.getLogger(__name__)
4751

@@ -62,7 +66,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
6266
) as exc:
6367
raise web.HTTPForbidden(reason=f"{exc}") from exc
6468

65-
except FoldersError as exc:
69+
except (FolderValueNotPermittedError, FoldersValueError) as exc:
6670
raise web.HTTPBadRequest(reason=f"{exc}") from exc
6771

6872
return wrapper

services/web/server/src/simcore_service_webserver/folders/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ class FoldersValueError(WebServerBaseError, ValueError):
55
...
66

77

8+
class FolderValueNotPermittedError(FoldersValueError):
9+
msg_template = "Provided value is not permitted. {reason}"
10+
11+
812
class FolderNotFoundError(FoldersValueError):
913
msg_template = "Folder not found. {reason}"
1014

services/web/server/tests/unit/with_dbs/03/folders/test_folders.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,19 @@ async def test_sub_folders_full_workflow(
181181
assert data[0]["name"] == "My sub sub folder"
182182
assert data[0]["parentFolderId"] == subfolder_folder["folderId"]
183183

184+
# try to move sub folder to sub sub folder (should not be allowed to)
185+
url = client.app.router["replace_folder"].url_for(
186+
folder_id=f"{subfolder_folder['folderId']}",
187+
)
188+
resp = await client.put(
189+
url.path,
190+
json={
191+
"name": "My Updated Folder",
192+
"parentFolderId": f"{subsubfolder_folder['folderId']}",
193+
},
194+
)
195+
await assert_status(resp, status.HTTP_400_BAD_REQUEST)
196+
184197
# move sub sub folder to root folder
185198
url = client.app.router["replace_folder"].url_for(
186199
folder_id=f"{subsubfolder_folder['folderId']}"

0 commit comments

Comments
 (0)