diff --git a/.env-devel b/.env-devel index 3f81b81ca52..22690e81871 100644 --- a/.env-devel +++ b/.env-devel @@ -32,7 +32,7 @@ AUTOSCALING_NODES_MONITORING=null AUTOSCALING_POLL_INTERVAL=10 AUTOSCALING_SSM_ACCESS=null -AWS_S3_CLI_S3='{"S3_ACCESS_KEY":"12345678", "S3_BUCKET_NAME":"simcore", "S3_ENDPOINT": "http://172.17.0.1:9001", "S3_SECRET_KEY": "12345678", "S3_REGION": "us-east-1"}' +AWS_S3_CLI_S3=null CATALOG_BACKGROUND_TASK_REST_TIME=60 CATALOG_DEV_FEATURES_ENABLED=0 diff --git a/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py b/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py index a3e52b0d7b9..e2d4918c435 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import NamedTuple +from models_library.access_rights import AccessRights from models_library.basic_types import IDStr from models_library.folders import FolderID from models_library.users import GroupID @@ -18,6 +19,8 @@ class FolderGet(OutputSchema): created_at: datetime modified_at: datetime owner: GroupID + workspace_id: WorkspaceID | None + my_access_rights: AccessRights class FolderGetPage(NamedTuple): diff --git a/packages/models-library/src/models_library/api_schemas_webserver/projects.py b/packages/models-library/src/models_library/api_schemas_webserver/projects.py index fdfe4fb0666..b644ac52d0b 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/projects.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/projects.py @@ -7,6 +7,7 @@ from typing import Any, Literal, TypeAlias +from models_library.folders import FolderID from models_library.workspaces import WorkspaceID from pydantic import Field, validator @@ -24,6 +25,7 @@ from ..utils.common_validators import ( empty_str_to_none_pre_validator, none_to_empty_str_pre_validator, + null_or_none_str_to_none_validator, ) from ..utils.pydantic_tools_extension import FieldNotRequired from ._base import EmptyModel, InputSchema, OutputSchema @@ -41,11 +43,16 @@ class ProjectCreateNew(InputSchema): classifiers: list[ClassifierID] = Field(default_factory=list) ui: StudyUI | None = None workspace_id: WorkspaceID | None = None + folder_id: FolderID | None = None _empty_is_none = validator( - "uuid", "thumbnail", "description", "workspace_id", allow_reuse=True, pre=True + "uuid", "thumbnail", "description", allow_reuse=True, pre=True )(empty_str_to_none_pre_validator) + _null_or_none_to_none = validator( + "workspace_id", "folder_id", allow_reuse=True, pre=True + )(null_or_none_str_to_none_validator) + # NOTE: based on OVERRIDABLE_DOCUMENT_KEYS class ProjectCopyOverride(InputSchema): diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 63abbd2dbd5..b948afdee14 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -8031,6 +8031,7 @@ components: - createdAt - modifiedAt - owner + - myAccessRights type: object properties: folderId: @@ -8059,6 +8060,13 @@ components: exclusiveMinimum: true type: integer minimum: 0 + workspaceId: + title: Workspaceid + exclusiveMinimum: true + type: integer + minimum: 0 + myAccessRights: + $ref: '#/components/schemas/models_library__access_rights__AccessRights' GenerateInvitation: title: GenerateInvitation required: @@ -8390,7 +8398,6 @@ components: issuer: title: Issuer type: string - format: email guest: title: Guest type: string @@ -9825,6 +9832,11 @@ components: exclusiveMinimum: true type: integer minimum: 0 + folderId: + title: Folderid + exclusiveMinimum: true + type: integer + minimum: 0 ProjectGet: title: ProjectGet required: diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_api.py b/services/web/server/src/simcore_service_webserver/folders/_folders_api.py index f54b3a9f4c2..44fe103edb7 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_api.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_api.py @@ -3,6 +3,7 @@ import logging from aiohttp import web +from models_library.access_rights import AccessRights from models_library.api_schemas_webserver.folders_v2 import FolderGet, FolderGetPage from models_library.folders import FolderID from models_library.products import ProductName @@ -35,8 +36,9 @@ async def create_folder( user = await get_user(app, user_id=user_id) workspace_is_private = True + user_folder_access_rights = AccessRights(read=True, write=True, delete=True) if workspace_id: - await check_user_workspace_access( + user_workspace_access_rights = await check_user_workspace_access( app, user_id=user_id, workspace_id=workspace_id, @@ -44,6 +46,7 @@ async def create_folder( permission="write", ) workspace_is_private = False + user_folder_access_rights = user_workspace_access_rights.my_access_rights # Check parent_folder_id lives in the workspace if parent_folder_id: @@ -86,6 +89,8 @@ async def create_folder( created_at=folder_db.created, modified_at=folder_db.modified, owner=folder_db.created_by_gid, + workspace_id=workspace_id, + my_access_rights=user_folder_access_rights, ) @@ -100,8 +105,9 @@ async def get_folder( ) workspace_is_private = True + user_folder_access_rights = AccessRights(read=True, write=True, delete=True) if folder_db.workspace_id: - await check_user_workspace_access( + user_workspace_access_rights = await check_user_workspace_access( app, user_id=user_id, workspace_id=folder_db.workspace_id, @@ -109,6 +115,7 @@ async def get_folder( permission="read", ) workspace_is_private = False + user_folder_access_rights = user_workspace_access_rights.my_access_rights folder_db = await folders_db.get_for_user_or_workspace( app, @@ -124,6 +131,8 @@ async def get_folder( created_at=folder_db.created, modified_at=folder_db.modified, owner=folder_db.created_by_gid, + workspace_id=folder_db.workspace_id, + my_access_rights=user_folder_access_rights, ) @@ -138,9 +147,10 @@ async def list_folders( order_by: OrderBy, ) -> FolderGetPage: workspace_is_private = True + user_folder_access_rights = AccessRights(read=True, write=True, delete=True) if workspace_id: - await check_user_workspace_access( + user_workspace_access_rights = await check_user_workspace_access( app, user_id=user_id, workspace_id=workspace_id, @@ -148,6 +158,7 @@ async def list_folders( permission="read", ) workspace_is_private = False + user_folder_access_rights = user_workspace_access_rights.my_access_rights if folder_id: # Check user access to folder @@ -178,6 +189,8 @@ async def list_folders( created_at=folder.created, modified_at=folder.modified, owner=folder.created_by_gid, + workspace_id=folder.workspace_id, + my_access_rights=user_folder_access_rights, ) for folder in folders ], @@ -199,8 +212,9 @@ async def update_folder( ) workspace_is_private = True + user_folder_access_rights = AccessRights(read=True, write=True, delete=True) if folder_db.workspace_id: - await check_user_workspace_access( + user_workspace_access_rights = await check_user_workspace_access( app, user_id=user_id, workspace_id=folder_db.workspace_id, @@ -208,9 +222,9 @@ async def update_folder( permission="write", ) workspace_is_private = False + user_folder_access_rights = user_workspace_access_rights.my_access_rights # Check user has acces to the folder - # NOTE: MD: TODO check function! await folders_db.get_for_user_or_workspace( app, folder_id=folder_id, @@ -233,6 +247,8 @@ async def update_folder( created_at=folder_db.created, modified_at=folder_db.modified, owner=folder_db.created_by_gid, + workspace_id=folder_db.workspace_id, + my_access_rights=user_folder_access_rights, ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_access_rights_api.py b/services/web/server/src/simcore_service_webserver/projects/_access_rights_api.py index c306e94f4e0..1c7919a4985 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_access_rights_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_access_rights_api.py @@ -35,10 +35,10 @@ async def get_user_project_access_rights( """ This function resolves user access rights on the project resource. - If project belong to user private workspace (workspace_id = None) then it is resolved + If project belongs to user private workspace (workspace_id = None) then it is resolved via user <--> groups <--> projects_to_groups. - If project belonsg to shared workspace (workspace_id not None) then it is resolved + If project belongs to shared workspace (workspace_id not None) then it is resolved via user <--> groups <--> workspace_access_rights """ db: ProjectDBAPI = app[APP_PROJECT_DBAPI] diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py index 780c976bd45..1cb21629baa 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py @@ -14,7 +14,7 @@ from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps -from models_library.workspaces import WorkspaceID +from models_library.workspaces import UserWorkspaceAccessRightsDB from pydantic import parse_obj_as from servicelib.aiohttp.long_running_tasks.server import TaskProgress from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON @@ -27,13 +27,16 @@ from ..application_settings import get_application_settings from ..catalog import client as catalog_client from ..director_v2 import api +from ..folders import _folders_db as folders_db from ..storage.api import ( copy_data_folders_from_project, get_project_total_size_simcore_s3, ) from ..users.api import get_user_fullname -from ..workspaces.api import get_workspace +from ..workspaces import _workspaces_db as workspaces_db +from ..workspaces.api import check_user_workspace_access from ..workspaces.errors import WorkspaceAccessForbiddenError +from . import _folders_db as project_to_folders_db from . import projects_api from ._metadata_api import set_project_ancestors from ._permalink_api import update_or_pop_permalink_in_project @@ -233,7 +236,6 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche simcore_user_agent: str, parent_project_uuid: ProjectID | None, parent_node_id: NodeID | None, - workspace_id: WorkspaceID | None, ) -> None: """Implements TaskProtocol for 'create_projects' handler @@ -264,8 +266,30 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche project_nodes = None try: task_progress.update(message="creating new study...") + + workspace_id = None + folder_id = None + if predefined_project: + if workspace_id := predefined_project.get("workspaceId", None): + await check_user_workspace_access( + request.app, + user_id=user_id, + workspace_id=workspace_id, + product_name=product_name, + permission="write", + ) + if folder_id := predefined_project.get("folderId", None): + # Check user has access to folder + await folders_db.get_for_user_or_workspace( + request.app, + folder_id=folder_id, + product_name=product_name, + user_id=user_id if workspace_id is None else None, + workspace_id=workspace_id, + ) + if from_study: - # 1. prepare copy + # 1.1 prepare copy ( new_project, project_node_coro, @@ -281,6 +305,19 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche if project_node_coro: project_nodes = await project_node_coro + # 1.2 does project belong to some folder? + workspace_id = new_project["workspaceId"] + prj_to_folder_db = await project_to_folders_db.get_project_to_folder( + request.app, + project_id=from_study, + private_workspace_user_id_or_none=( + user_id if workspace_id is None else None + ), + ) + if prj_to_folder_db: + # As user has access to the project, it has implicitly access to the folder + folder_id = prj_to_folder_db.folder_id + if predefined_project: # 2. overrides with optional body and re-validate new_project, project_nodes = await _compose_project_data( @@ -290,21 +327,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche predefined_project=predefined_project, ) - # If user wants to create project in specific workspace - if workspace_id: - # Verify user access to the specified workspace; raise an error if access is denied - workspace = await get_workspace( - request.app, - user_id=user_id, - workspace_id=workspace_id, - product_name=product_name, - ) - if workspace.my_access_rights.write is False: - raise WorkspaceAccessForbiddenError( - reason=f"User {user_id} does not have write permission on workspace {workspace_id}." - ) - - # 3. save new project in DB + # 3.1 save new project in DB new_project = await db.insert_project( project=jsonable_encoder(new_project), user_id=user_id, @@ -323,6 +346,17 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche ) task_progress.update() + # 3.2 move project to proper folder + if folder_id: + await project_to_folders_db.insert_project_to_folder( + request.app, + project_id=new_project["uuid"], + folder_id=folder_id, + private_workspace_user_id_or_none=( + user_id if workspace_id is None else None + ), + ) + # 4. deep copy source project's files if copy_file_coro: # NOTE: storage needs to have access to the new project prior to copying files @@ -356,6 +390,20 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche # Adds permalink await update_or_pop_permalink_in_project(request, new_project) + # Overwrite project access rights + if workspace_id: + workspace_db: UserWorkspaceAccessRightsDB = ( + await workspaces_db.get_workspace_for_user( + app=request.app, + user_id=user_id, + workspace_id=workspace_id, + product_name=product_name, + ) + ) + new_project["accessRights"] = { + gid: access.dict() for gid, access in workspace_db.access_rights.items() + } + # Ensures is like ProjectGet data = ProjectGet.parse_obj(new_project).data(exclude_unset=True) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py index 2c9df781336..7efd1dd600c 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_read.py @@ -5,13 +5,15 @@ """ + from aiohttp import web +from models_library.access_rights import AccessRights from models_library.api_schemas_webserver._base import OutputSchema from models_library.api_schemas_webserver.projects import ProjectListItem from models_library.folders import FolderID from models_library.projects import ProjectID from models_library.rest_ordering import OrderBy -from models_library.users import UserID +from models_library.users import GroupID, UserID from models_library.workspaces import WorkspaceID from pydantic import NonNegativeInt from servicelib.utils import logged_gather @@ -22,6 +24,7 @@ from ..catalog.client import get_services_for_user_in_product from ..folders import _folders_db as folders_db +from ..workspaces import _workspaces_db as workspaces_db from . import projects_api from ._permalink_api import update_or_pop_permalink_in_project from .db import ProjectDBAPI @@ -34,6 +37,7 @@ async def _append_fields( user_id: UserID, project: ProjectDict, is_template: bool, + workspace_access_rights: dict[GroupID, AccessRights] | None, model_schema_cls: type[OutputSchema], ): # state @@ -47,6 +51,12 @@ async def _append_fields( # permalink await update_or_pop_permalink_in_project(request, project) + # replace project access rights (if project is in workspace) + if workspace_access_rights: + project["accessRights"] = { + gid: access.dict() for gid, access in workspace_access_rights.items() + } + # validate return model_schema_cls.parse_obj(project).data(exclude_unset=True) @@ -106,6 +116,14 @@ async def list_projects( # pylint: disable=too-many-arguments workspace_id=workspace_id, ) + # If workspace, override project access rights + workspace_access_rights = None + if workspace_id: + workspace_db = await workspaces_db.get_workspace_for_user( + app, user_id=user_id, workspace_id=workspace_id, product_name=product_name + ) + workspace_access_rights = workspace_db.access_rights + projects: list[ProjectDict] = await logged_gather( *( _append_fields( @@ -113,6 +131,7 @@ async def list_projects( # pylint: disable=too-many-arguments user_id=user_id, project=prj, is_template=prj_type == ProjectTypeDB.TEMPLATE, + workspace_access_rights=workspace_access_rights, model_schema_cls=ProjectListItem, ) for prj, prj_type in zip(db_projects, db_project_types) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index 16e7f9574ad..9f138ebbb97 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -138,7 +138,6 @@ async def create_project(request: web.Request): # :create, :copy (w/ and w/o override) # NOTE: see clone_project - workspace_id = None if not request.can_read_body: # request w/o body predefined_project = None @@ -158,10 +157,6 @@ async def create_project(request: web.Request): or None ) - # # Manually include workspace after exclude - # workspace_id = project_create.dict(by_alias=True).get("workspaceId", None) - # predefined_project["workspaceId"] = workspace_id - return await start_long_running_task( request, _crud_api_create.create_project, # type: ignore[arg-type] # @GitHK, @pcrespov this one I don't know how to fix @@ -179,7 +174,6 @@ async def create_project(request: web.Request): predefined_project=predefined_project, parent_project_uuid=header_params.parent_project_uuid, parent_node_id=header_params.parent_node_id, - workspace_id=workspace_id, ) @@ -631,12 +625,6 @@ async def clone_project(request: web.Request): req_ctx = RequestContext.parse_obj(request) path_params = parse_request_path_parameters_as(ProjectPathParams, request) - db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(request.app) - try: - project_db = await db.get_project_db(path_params.project_id) - except ProjectNotFoundError as exc: - raise web.HTTPNotFound(reason=f"Project {exc.project_uuid} not found") from exc - return await start_long_running_task( request, _crud_api_create.create_project, # type: ignore[arg-type] # @GitHK, @pcrespov this one I don't know how to fix @@ -656,5 +644,4 @@ async def clone_project(request: web.Request): predefined_project=None, parent_project_uuid=None, parent_node_id=None, - workspace_id=project_db.workspace_id, ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_folders_api.py b/services/web/server/src/simcore_service_webserver/projects/_folders_api.py index d2107b46499..4b465edf0e9 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_folders_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_folders_api.py @@ -82,7 +82,7 @@ async def move_project_into_folder( app, project_id=project_id, folder_id=folder_id, - private_workspace_user_id_or_none=user_id - if workspace_is_private - else None, + private_workspace_user_id_or_none=( + user_id if workspace_is_private else None + ), ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_tags_api.py b/services/web/server/src/simcore_service_webserver/projects/_tags_api.py index 60f08dd1387..ba4be3c5fb4 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_tags_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/_tags_api.py @@ -7,7 +7,9 @@ from aiohttp import web from models_library.projects import ProjectID from models_library.users import UserID +from models_library.workspaces import UserWorkspaceAccessRightsDB +from ..workspaces import _workspaces_db as workspaces_db from ._access_rights_api import check_user_project_permission from .db import ProjectDBAPI from .models import ProjectDict @@ -32,6 +34,20 @@ async def add_tag( project: ProjectDict = await db.add_tag( project_uuid=f"{project_uuid}", user_id=user_id, tag_id=int(tag_id) ) + + if project["workspaceId"] is not None: + workspace_db: UserWorkspaceAccessRightsDB = ( + await workspaces_db.get_workspace_for_user( + app=app, + user_id=user_id, + workspace_id=project["workspaceId"], + product_name=product_name, + ) + ) + project["accessRights"] = { + gid: access.dict() for gid, access in workspace_db.access_rights.items() + } + return project @@ -52,4 +68,18 @@ async def remove_tag( project: ProjectDict = await db.remove_tag( project_uuid=f"{project_uuid}", user_id=user_id, tag_id=tag_id ) + + if project["workspaceId"] is not None: + workspace_db: UserWorkspaceAccessRightsDB = ( + await workspaces_db.get_workspace_for_user( + app=app, + user_id=user_id, + workspace_id=project["workspaceId"], + product_name=product_name, + ) + ) + project["accessRights"] = { + gid: access.dict() for gid, access in workspace_db.access_rights.items() + } + return project diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_api.py b/services/web/server/src/simcore_service_webserver/projects/projects_api.py index 18cac2b32f9..fc130436caf 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_api.py @@ -62,6 +62,7 @@ from models_library.utils.fastapi_encoders import jsonable_encoder from models_library.utils.json_serialization import json_dumps from models_library.wallets import ZERO_CREDITS, WalletID, WalletInfo +from models_library.workspaces import UserWorkspaceAccessRightsDB from pydantic import ByteSize, parse_obj_as from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY from servicelib.common_headers import ( @@ -118,6 +119,7 @@ ) from ..wallets import api as wallets_api from ..wallets.errors import WalletNotEnoughCreditsError +from ..workspaces import _workspaces_db as workspaces_db from . import _crud_api_delete, _nodes_api from ._access_rights_api import ( check_user_project_permission, @@ -196,7 +198,18 @@ async def get_project_for_user( user_id, project, project_type is ProjectType.TEMPLATE, app ) - # If from workspace -> hack workspace permissions + if project["workspaceId"] is not None: + workspace_db: UserWorkspaceAccessRightsDB = ( + await workspaces_db.get_workspace_for_user( + app=app, + user_id=user_id, + workspace_id=project["workspaceId"], + product_name=product_name, + ) + ) + project["accessRights"] = { + gid: access.dict() for gid, access in workspace_db.access_rights.items() + } Project.parse_obj(project) # NOTE: only validates return project diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py index ac13e33581e..98ef90886ac 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_db.py @@ -132,9 +132,7 @@ async def list_workspaces_for_user( my_access_rights_subquery.c.my_access_rights, ) .select_from( - workspaces.join(access_rights_subquery, isouter=True).join( - my_access_rights_subquery - ) + workspaces.join(access_rights_subquery).join(my_access_rights_subquery) ) .where(workspaces.c.product_name == product_name) ) diff --git a/services/web/server/src/simcore_service_webserver/workspaces/api.py b/services/web/server/src/simcore_service_webserver/workspaces/api.py index d3643161876..2b3ca3bbd42 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/api.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/api.py @@ -1,6 +1,10 @@ # mypy: disable-error-code=truthy-function -from ._workspaces_api import get_workspace +from ._workspaces_api import check_user_workspace_access, get_workspace assert get_workspace # nosec +assert check_user_workspace_access # nosec -__all__: tuple[str, ...] = ("get_workspace",) +__all__: tuple[str, ...] = ( + "get_workspace", + "check_user_workspace_access", +) diff --git a/services/web/server/tests/conftest.py b/services/web/server/tests/conftest.py index 4c9a38a7b0d..3f47a75afc2 100644 --- a/services/web/server/tests/conftest.py +++ b/services/web/server/tests/conftest.py @@ -226,6 +226,7 @@ async def _setup( "name": None, "prjOwner": None, "workspaceId": None, + "folderId": None, } if from_study: # access rights are replaced diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py index be67e5c5bc9..6ca7392dd4b 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone.py @@ -16,7 +16,11 @@ from models_library.projects import ProjectID from pydantic import parse_obj_as from pytest_simcore.helpers.webserver_login import UserInfoDict -from pytest_simcore.helpers.webserver_parametrizations import MockedStorageSubsystem +from pytest_simcore.helpers.webserver_parametrizations import ( + MockedStorageSubsystem, + standard_role_response, +) +from servicelib.aiohttp import status from servicelib.aiohttp.long_running_tasks.client import long_running_task_request from simcore_service_webserver.db.models import UserRole from simcore_service_webserver.projects.models import ProjectDict @@ -48,6 +52,34 @@ async def _request_clone_project(client: TestClient, url: URL) -> ProjectGet: return ProjectGet.parse_obj(data) +@pytest.mark.parametrize(*standard_role_response(), ids=str) +async def test_clone_project_user_permissions( + client: TestClient, + logged_user: UserInfoDict, + user_project: ProjectDict, + # mocks backend + storage_subsystem_mock: MockedStorageSubsystem, + mock_catalog_service_api_responses: None, + project_db_cleaner: None, + expected, +): + assert client.app + + project = user_project + + url = client.app.router["clone_project"].url_for(project_id=project["uuid"]) + assert f"/v0/projects/{project['uuid']}:clone" == url.path + + try: + cloned_project = await _request_clone_project(client, url) + except Exception as exc: # pylint: disable=broad-exception-caught + assert exc.status == expected.ok # pylint: disable=no-member + + if expected.ok == status.HTTP_200_OK: + # check whether it's a clone + assert ProjectID(project["uuid"]) != cloned_project.uuid + + @pytest.mark.parametrize( "user_role", [UserRole.USER], diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone_in_workspace_and_folder.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone_in_workspace_and_folder.py new file mode 100644 index 00000000000..763976e2504 --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers__clone_in_workspace_and_folder.py @@ -0,0 +1,141 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +# pylint: disable=unused-argument +# pylint: disable=unused-variable + +from copy import deepcopy +from typing import Any, Iterator + +import pytest +import sqlalchemy as sa +from aiohttp.test_utils import TestClient +from models_library.api_schemas_webserver.projects import ProjectGet +from models_library.folders import FolderID +from models_library.projects import ProjectID +from models_library.workspaces import WorkspaceID +from pytest_simcore.helpers.webserver_login import UserInfoDict +from pytest_simcore.helpers.webserver_parametrizations import MockedStorageSubsystem +from servicelib.aiohttp.long_running_tasks.client import long_running_task_request +from simcore_postgres_database.models.folders import folders +from simcore_postgres_database.models.workspaces import workspaces +from simcore_service_webserver.db.models import UserRole +from simcore_service_webserver.folders._folders_api import create_folder +from simcore_service_webserver.projects._folders_api import move_project_into_folder +from simcore_service_webserver.projects.models import ProjectDict +from simcore_service_webserver.workspaces._workspaces_api import create_workspace +from yarl import URL + + +@pytest.fixture +async def create_workspace_and_folder( + client: TestClient, logged_user: UserInfoDict, postgres_db: sa.engine.Engine +) -> Iterator[tuple[WorkspaceID, FolderID]]: + workspace = await create_workspace( + client.app, + user_id=logged_user["id"], + name="a", + description=None, + thumbnail=None, + product_name="osparc", + ) + + folder = await create_folder( + client.app, + user_id=logged_user["id"], + name="a", + parent_folder_id=None, + product_name="osparc", + workspace_id=workspace.workspace_id, + ) + + yield (workspace.workspace_id, folder.folder_id) + + with postgres_db.connect() as con: + con.execute(folders.delete()) + con.execute(workspaces.delete()) + + +@pytest.fixture +def fake_project( + fake_project: ProjectDict, + workbench_db_column: dict[str, Any], + create_workspace_and_folder: tuple[WorkspaceID, FolderID], +) -> ProjectDict: + # OVERRIDES user_project + project = deepcopy(fake_project) + project["workbench"] = workbench_db_column + project["workspaceId"] = create_workspace_and_folder[0] + return project + + +async def _request_clone_project(client: TestClient, url: URL) -> ProjectGet: + """Raise HTTPError subclasses if request fails""" + # polls until long-running task is done + data = None + async for long_running_task in long_running_task_request( + client.session, url=client.make_url(url.path), json=None, client_timeout=30 + ): + print(f"{long_running_task.progress=}") + if long_running_task.done(): + data = await long_running_task.result() + + assert data is not None + return ProjectGet.parse_obj(data) + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_clone_project( + client: TestClient, + logged_user: UserInfoDict, + user_project: ProjectDict, + # mocks backend + storage_subsystem_mock: MockedStorageSubsystem, + mock_catalog_service_api_responses: None, + project_db_cleaner: None, + create_workspace_and_folder: tuple[WorkspaceID, FolderID], +): + assert client.app + + project = user_project + await move_project_into_folder( + client.app, + user_id=logged_user["id"], + project_id=project["uuid"], + folder_id=create_workspace_and_folder[1], + product_name="osparc", + ) + + base_url = client.app.router["list_projects"].url_for() + query_parameters = { + "workspace_id": f"{create_workspace_and_folder[0]}", + "folder_id": f"{create_workspace_and_folder[1]}", + } + url = base_url.with_query(**query_parameters) + resp = await client.get(url) + data = await resp.json() + assert resp.status == 200 + assert len(data["data"]) == 1 + + url = client.app.router["clone_project"].url_for(project_id=project["uuid"]) + assert f"/v0/projects/{project['uuid']}:clone" == url.path + + cloned_project = await _request_clone_project(client, url) + + # check whether it's a clone + assert ProjectID(project["uuid"]) != cloned_project.uuid + assert cloned_project.workspace_id == create_workspace_and_folder[0] + + # check whether it's in right folder + base_url = client.app.router["list_projects"].url_for() + query_parameters = { + "workspace_id": f"{create_workspace_and_folder[0]}", + "folder_id": f"{create_workspace_and_folder[1]}", + } + url = base_url.with_query(**query_parameters) + resp = await client.get(url) + data = await resp.json() + assert resp.status == 200 + assert len(data["data"]) == 2 diff --git a/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py b/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py index acd3b1f96b9..16cc4f667fb 100644 --- a/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py +++ b/services/web/server/tests/unit/with_dbs/03/folders/test_folders.py @@ -74,6 +74,7 @@ async def test_folders_full_workflow( ) resp = await client.get(url) data, _ = await assert_status(resp, status.HTTP_200_OK) + assert FolderGet.parse_obj(data) assert data["folderId"] == added_folder["folderId"] assert data["name"] == "My first folder"