Skip to content

Commit 1c16908

Browse files
🎨 Improve worskpace feature (1. Part) (#6303)
1 parent 2912fe6 commit 1c16908

File tree

18 files changed

+365
-53
lines changed

18 files changed

+365
-53
lines changed

.env-devel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ AUTOSCALING_NODES_MONITORING=null
3232
AUTOSCALING_POLL_INTERVAL=10
3333
AUTOSCALING_SSM_ACCESS=null
3434

35-
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"}'
35+
AWS_S3_CLI_S3=null
3636

3737
CATALOG_BACKGROUND_TASK_REST_TIME=60
3838
CATALOG_DEV_FEATURES_ENABLED=0

packages/models-library/src/models_library/api_schemas_webserver/folders_v2.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from typing import NamedTuple
33

4+
from models_library.access_rights import AccessRights
45
from models_library.basic_types import IDStr
56
from models_library.folders import FolderID
67
from models_library.users import GroupID
@@ -18,6 +19,8 @@ class FolderGet(OutputSchema):
1819
created_at: datetime
1920
modified_at: datetime
2021
owner: GroupID
22+
workspace_id: WorkspaceID | None
23+
my_access_rights: AccessRights
2124

2225

2326
class FolderGetPage(NamedTuple):

packages/models-library/src/models_library/api_schemas_webserver/projects.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from typing import Any, Literal, TypeAlias
99

10+
from models_library.folders import FolderID
1011
from models_library.workspaces import WorkspaceID
1112
from pydantic import Field, validator
1213

@@ -24,6 +25,7 @@
2425
from ..utils.common_validators import (
2526
empty_str_to_none_pre_validator,
2627
none_to_empty_str_pre_validator,
28+
null_or_none_str_to_none_validator,
2729
)
2830
from ..utils.pydantic_tools_extension import FieldNotRequired
2931
from ._base import EmptyModel, InputSchema, OutputSchema
@@ -41,11 +43,16 @@ class ProjectCreateNew(InputSchema):
4143
classifiers: list[ClassifierID] = Field(default_factory=list)
4244
ui: StudyUI | None = None
4345
workspace_id: WorkspaceID | None = None
46+
folder_id: FolderID | None = None
4447

4548
_empty_is_none = validator(
46-
"uuid", "thumbnail", "description", "workspace_id", allow_reuse=True, pre=True
49+
"uuid", "thumbnail", "description", allow_reuse=True, pre=True
4750
)(empty_str_to_none_pre_validator)
4851

52+
_null_or_none_to_none = validator(
53+
"workspace_id", "folder_id", allow_reuse=True, pre=True
54+
)(null_or_none_str_to_none_validator)
55+
4956

5057
# NOTE: based on OVERRIDABLE_DOCUMENT_KEYS
5158
class ProjectCopyOverride(InputSchema):

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8031,6 +8031,7 @@ components:
80318031
- createdAt
80328032
- modifiedAt
80338033
- owner
8034+
- myAccessRights
80348035
type: object
80358036
properties:
80368037
folderId:
@@ -8059,6 +8060,13 @@ components:
80598060
exclusiveMinimum: true
80608061
type: integer
80618062
minimum: 0
8063+
workspaceId:
8064+
title: Workspaceid
8065+
exclusiveMinimum: true
8066+
type: integer
8067+
minimum: 0
8068+
myAccessRights:
8069+
$ref: '#/components/schemas/models_library__access_rights__AccessRights'
80628070
GenerateInvitation:
80638071
title: GenerateInvitation
80648072
required:
@@ -8390,7 +8398,6 @@ components:
83908398
issuer:
83918399
title: Issuer
83928400
type: string
8393-
format: email
83948401
guest:
83958402
title: Guest
83968403
type: string
@@ -9825,6 +9832,11 @@ components:
98259832
exclusiveMinimum: true
98269833
type: integer
98279834
minimum: 0
9835+
folderId:
9836+
title: Folderid
9837+
exclusiveMinimum: true
9838+
type: integer
9839+
minimum: 0
98289840
ProjectGet:
98299841
title: ProjectGet
98309842
required:

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44

55
from aiohttp import web
6+
from models_library.access_rights import AccessRights
67
from models_library.api_schemas_webserver.folders_v2 import FolderGet, FolderGetPage
78
from models_library.folders import FolderID
89
from models_library.products import ProductName
@@ -35,15 +36,17 @@ async def create_folder(
3536
user = await get_user(app, user_id=user_id)
3637

3738
workspace_is_private = True
39+
user_folder_access_rights = AccessRights(read=True, write=True, delete=True)
3840
if workspace_id:
39-
await check_user_workspace_access(
41+
user_workspace_access_rights = await check_user_workspace_access(
4042
app,
4143
user_id=user_id,
4244
workspace_id=workspace_id,
4345
product_name=product_name,
4446
permission="write",
4547
)
4648
workspace_is_private = False
49+
user_folder_access_rights = user_workspace_access_rights.my_access_rights
4750

4851
# Check parent_folder_id lives in the workspace
4952
if parent_folder_id:
@@ -86,6 +89,8 @@ async def create_folder(
8689
created_at=folder_db.created,
8790
modified_at=folder_db.modified,
8891
owner=folder_db.created_by_gid,
92+
workspace_id=workspace_id,
93+
my_access_rights=user_folder_access_rights,
8994
)
9095

9196

@@ -100,15 +105,17 @@ async def get_folder(
100105
)
101106

102107
workspace_is_private = True
108+
user_folder_access_rights = AccessRights(read=True, write=True, delete=True)
103109
if folder_db.workspace_id:
104-
await check_user_workspace_access(
110+
user_workspace_access_rights = await check_user_workspace_access(
105111
app,
106112
user_id=user_id,
107113
workspace_id=folder_db.workspace_id,
108114
product_name=product_name,
109115
permission="read",
110116
)
111117
workspace_is_private = False
118+
user_folder_access_rights = user_workspace_access_rights.my_access_rights
112119

113120
folder_db = await folders_db.get_for_user_or_workspace(
114121
app,
@@ -124,6 +131,8 @@ async def get_folder(
124131
created_at=folder_db.created,
125132
modified_at=folder_db.modified,
126133
owner=folder_db.created_by_gid,
134+
workspace_id=folder_db.workspace_id,
135+
my_access_rights=user_folder_access_rights,
127136
)
128137

129138

@@ -138,16 +147,18 @@ async def list_folders(
138147
order_by: OrderBy,
139148
) -> FolderGetPage:
140149
workspace_is_private = True
150+
user_folder_access_rights = AccessRights(read=True, write=True, delete=True)
141151

142152
if workspace_id:
143-
await check_user_workspace_access(
153+
user_workspace_access_rights = await check_user_workspace_access(
144154
app,
145155
user_id=user_id,
146156
workspace_id=workspace_id,
147157
product_name=product_name,
148158
permission="read",
149159
)
150160
workspace_is_private = False
161+
user_folder_access_rights = user_workspace_access_rights.my_access_rights
151162

152163
if folder_id:
153164
# Check user access to folder
@@ -178,6 +189,8 @@ async def list_folders(
178189
created_at=folder.created,
179190
modified_at=folder.modified,
180191
owner=folder.created_by_gid,
192+
workspace_id=folder.workspace_id,
193+
my_access_rights=user_folder_access_rights,
181194
)
182195
for folder in folders
183196
],
@@ -199,18 +212,19 @@ async def update_folder(
199212
)
200213

201214
workspace_is_private = True
215+
user_folder_access_rights = AccessRights(read=True, write=True, delete=True)
202216
if folder_db.workspace_id:
203-
await check_user_workspace_access(
217+
user_workspace_access_rights = await check_user_workspace_access(
204218
app,
205219
user_id=user_id,
206220
workspace_id=folder_db.workspace_id,
207221
product_name=product_name,
208222
permission="write",
209223
)
210224
workspace_is_private = False
225+
user_folder_access_rights = user_workspace_access_rights.my_access_rights
211226

212227
# Check user has acces to the folder
213-
# NOTE: MD: TODO check function!
214228
await folders_db.get_for_user_or_workspace(
215229
app,
216230
folder_id=folder_id,
@@ -233,6 +247,8 @@ async def update_folder(
233247
created_at=folder_db.created,
234248
modified_at=folder_db.modified,
235249
owner=folder_db.created_by_gid,
250+
workspace_id=folder_db.workspace_id,
251+
my_access_rights=user_folder_access_rights,
236252
)
237253

238254

services/web/server/src/simcore_service_webserver/projects/_access_rights_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ async def get_user_project_access_rights(
3535
"""
3636
This function resolves user access rights on the project resource.
3737
38-
If project belong to user private workspace (workspace_id = None) then it is resolved
38+
If project belongs to user private workspace (workspace_id = None) then it is resolved
3939
via user <--> groups <--> projects_to_groups.
4040
41-
If project belonsg to shared workspace (workspace_id not None) then it is resolved
41+
If project belongs to shared workspace (workspace_id not None) then it is resolved
4242
via user <--> groups <--> workspace_access_rights
4343
"""
4444
db: ProjectDBAPI = app[APP_PROJECT_DBAPI]

services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from models_library.users import UserID
1515
from models_library.utils.fastapi_encoders import jsonable_encoder
1616
from models_library.utils.json_serialization import json_dumps
17-
from models_library.workspaces import WorkspaceID
17+
from models_library.workspaces import UserWorkspaceAccessRightsDB
1818
from pydantic import parse_obj_as
1919
from servicelib.aiohttp.long_running_tasks.server import TaskProgress
2020
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
@@ -27,13 +27,16 @@
2727
from ..application_settings import get_application_settings
2828
from ..catalog import client as catalog_client
2929
from ..director_v2 import api
30+
from ..folders import _folders_db as folders_db
3031
from ..storage.api import (
3132
copy_data_folders_from_project,
3233
get_project_total_size_simcore_s3,
3334
)
3435
from ..users.api import get_user_fullname
35-
from ..workspaces.api import get_workspace
36+
from ..workspaces import _workspaces_db as workspaces_db
37+
from ..workspaces.api import check_user_workspace_access
3638
from ..workspaces.errors import WorkspaceAccessForbiddenError
39+
from . import _folders_db as project_to_folders_db
3740
from . import projects_api
3841
from ._metadata_api import set_project_ancestors
3942
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
233236
simcore_user_agent: str,
234237
parent_project_uuid: ProjectID | None,
235238
parent_node_id: NodeID | None,
236-
workspace_id: WorkspaceID | None,
237239
) -> None:
238240
"""Implements TaskProtocol for 'create_projects' handler
239241
@@ -264,8 +266,30 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
264266
project_nodes = None
265267
try:
266268
task_progress.update(message="creating new study...")
269+
270+
workspace_id = None
271+
folder_id = None
272+
if predefined_project:
273+
if workspace_id := predefined_project.get("workspaceId", None):
274+
await check_user_workspace_access(
275+
request.app,
276+
user_id=user_id,
277+
workspace_id=workspace_id,
278+
product_name=product_name,
279+
permission="write",
280+
)
281+
if folder_id := predefined_project.get("folderId", None):
282+
# Check user has access to folder
283+
await folders_db.get_for_user_or_workspace(
284+
request.app,
285+
folder_id=folder_id,
286+
product_name=product_name,
287+
user_id=user_id if workspace_id is None else None,
288+
workspace_id=workspace_id,
289+
)
290+
267291
if from_study:
268-
# 1. prepare copy
292+
# 1.1 prepare copy
269293
(
270294
new_project,
271295
project_node_coro,
@@ -281,6 +305,19 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
281305
if project_node_coro:
282306
project_nodes = await project_node_coro
283307

308+
# 1.2 does project belong to some folder?
309+
workspace_id = new_project["workspaceId"]
310+
prj_to_folder_db = await project_to_folders_db.get_project_to_folder(
311+
request.app,
312+
project_id=from_study,
313+
private_workspace_user_id_or_none=(
314+
user_id if workspace_id is None else None
315+
),
316+
)
317+
if prj_to_folder_db:
318+
# As user has access to the project, it has implicitly access to the folder
319+
folder_id = prj_to_folder_db.folder_id
320+
284321
if predefined_project:
285322
# 2. overrides with optional body and re-validate
286323
new_project, project_nodes = await _compose_project_data(
@@ -290,21 +327,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
290327
predefined_project=predefined_project,
291328
)
292329

293-
# If user wants to create project in specific workspace
294-
if workspace_id:
295-
# Verify user access to the specified workspace; raise an error if access is denied
296-
workspace = await get_workspace(
297-
request.app,
298-
user_id=user_id,
299-
workspace_id=workspace_id,
300-
product_name=product_name,
301-
)
302-
if workspace.my_access_rights.write is False:
303-
raise WorkspaceAccessForbiddenError(
304-
reason=f"User {user_id} does not have write permission on workspace {workspace_id}."
305-
)
306-
307-
# 3. save new project in DB
330+
# 3.1 save new project in DB
308331
new_project = await db.insert_project(
309332
project=jsonable_encoder(new_project),
310333
user_id=user_id,
@@ -323,6 +346,17 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche
323346
)
324347
task_progress.update()
325348

349+
# 3.2 move project to proper folder
350+
if folder_id:
351+
await project_to_folders_db.insert_project_to_folder(
352+
request.app,
353+
project_id=new_project["uuid"],
354+
folder_id=folder_id,
355+
private_workspace_user_id_or_none=(
356+
user_id if workspace_id is None else None
357+
),
358+
)
359+
326360
# 4. deep copy source project's files
327361
if copy_file_coro:
328362
# 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
356390
# Adds permalink
357391
await update_or_pop_permalink_in_project(request, new_project)
358392

393+
# Overwrite project access rights
394+
if workspace_id:
395+
workspace_db: UserWorkspaceAccessRightsDB = (
396+
await workspaces_db.get_workspace_for_user(
397+
app=request.app,
398+
user_id=user_id,
399+
workspace_id=workspace_id,
400+
product_name=product_name,
401+
)
402+
)
403+
new_project["accessRights"] = {
404+
gid: access.dict() for gid, access in workspace_db.access_rights.items()
405+
}
406+
359407
# Ensures is like ProjectGet
360408
data = ProjectGet.parse_obj(new_project).data(exclude_unset=True)
361409

0 commit comments

Comments
 (0)