fractal-server 2.17.2__py3-none-any.whl → 2.18.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +2 -1
- fractal_server/app/models/linkuserproject.py +40 -0
- fractal_server/app/models/security.py +7 -5
- fractal_server/app/models/v2/job.py +13 -2
- fractal_server/app/models/v2/resource.py +13 -0
- fractal_server/app/routes/admin/v2/__init__.py +11 -11
- fractal_server/app/routes/admin/v2/accounting.py +2 -2
- fractal_server/app/routes/admin/v2/job.py +34 -23
- fractal_server/app/routes/admin/v2/sharing.py +103 -0
- fractal_server/app/routes/admin/v2/task.py +9 -8
- fractal_server/app/routes/admin/v2/task_group.py +94 -16
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +20 -20
- fractal_server/app/routes/api/__init__.py +0 -9
- fractal_server/app/routes/api/v2/__init__.py +47 -47
- fractal_server/app/routes/api/v2/_aux_functions.py +65 -64
- fractal_server/app/routes/api/v2/_aux_functions_history.py +8 -3
- fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
- fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +4 -4
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +2 -2
- fractal_server/app/routes/api/v2/dataset.py +89 -77
- fractal_server/app/routes/api/v2/history.py +28 -16
- fractal_server/app/routes/api/v2/images.py +22 -8
- fractal_server/app/routes/api/v2/job.py +40 -24
- fractal_server/app/routes/api/v2/pre_submission_checks.py +13 -6
- fractal_server/app/routes/api/v2/project.py +48 -25
- fractal_server/app/routes/api/v2/sharing.py +311 -0
- fractal_server/app/routes/api/v2/status_legacy.py +22 -33
- fractal_server/app/routes/api/v2/submit.py +76 -71
- fractal_server/app/routes/api/v2/task.py +15 -17
- fractal_server/app/routes/api/v2/task_collection.py +18 -18
- fractal_server/app/routes/api/v2/task_collection_custom.py +11 -13
- fractal_server/app/routes/api/v2/task_collection_pixi.py +9 -9
- fractal_server/app/routes/api/v2/task_group.py +18 -18
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +26 -26
- fractal_server/app/routes/api/v2/task_version_update.py +12 -9
- fractal_server/app/routes/api/v2/workflow.py +41 -29
- fractal_server/app/routes/api/v2/workflow_import.py +25 -23
- fractal_server/app/routes/api/v2/workflowtask.py +25 -17
- fractal_server/app/routes/auth/_aux_auth.py +100 -0
- fractal_server/app/routes/auth/current_user.py +0 -63
- fractal_server/app/routes/auth/group.py +1 -30
- fractal_server/app/routes/auth/router.py +2 -0
- fractal_server/app/routes/auth/users.py +9 -0
- fractal_server/app/routes/auth/viewer_paths.py +43 -0
- fractal_server/app/schemas/user.py +29 -12
- fractal_server/app/schemas/user_group.py +0 -15
- fractal_server/app/schemas/v2/__init__.py +55 -48
- fractal_server/app/schemas/v2/dataset.py +35 -13
- fractal_server/app/schemas/v2/dumps.py +9 -9
- fractal_server/app/schemas/v2/job.py +11 -11
- fractal_server/app/schemas/v2/project.py +3 -3
- fractal_server/app/schemas/v2/resource.py +13 -4
- fractal_server/app/schemas/v2/sharing.py +99 -0
- fractal_server/app/schemas/v2/status_legacy.py +3 -3
- fractal_server/app/schemas/v2/task.py +6 -6
- fractal_server/app/schemas/v2/task_collection.py +4 -4
- fractal_server/app/schemas/v2/task_group.py +16 -16
- fractal_server/app/schemas/v2/workflow.py +16 -16
- fractal_server/app/schemas/v2/workflowtask.py +14 -14
- fractal_server/app/security/__init__.py +1 -1
- fractal_server/app/shutdown.py +6 -6
- fractal_server/config/__init__.py +0 -6
- fractal_server/config/_data.py +0 -79
- fractal_server/config/_main.py +6 -1
- fractal_server/data_migrations/2_18_0.py +30 -0
- fractal_server/images/models.py +1 -2
- fractal_server/main.py +72 -11
- fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
- fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
- fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
- fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
- fractal_server/runner/config/_slurm.py +2 -0
- fractal_server/runner/executors/slurm_common/_batching.py +4 -10
- fractal_server/runner/executors/slurm_common/slurm_config.py +1 -0
- fractal_server/runner/executors/slurm_ssh/runner.py +1 -1
- fractal_server/runner/executors/slurm_sudo/runner.py +1 -1
- fractal_server/runner/v2/_local.py +4 -3
- fractal_server/runner/v2/_slurm_ssh.py +4 -3
- fractal_server/runner/v2/_slurm_sudo.py +4 -3
- fractal_server/runner/v2/runner.py +36 -17
- fractal_server/runner/v2/runner_functions.py +11 -14
- fractal_server/runner/v2/submit_workflow.py +22 -9
- fractal_server/tasks/v2/local/_utils.py +2 -2
- fractal_server/tasks/v2/local/collect.py +5 -6
- fractal_server/tasks/v2/local/collect_pixi.py +5 -6
- fractal_server/tasks/v2/local/deactivate.py +7 -7
- fractal_server/tasks/v2/local/deactivate_pixi.py +3 -3
- fractal_server/tasks/v2/local/delete.py +5 -5
- fractal_server/tasks/v2/local/reactivate.py +5 -5
- fractal_server/tasks/v2/local/reactivate_pixi.py +5 -5
- fractal_server/tasks/v2/ssh/collect.py +5 -5
- fractal_server/tasks/v2/ssh/collect_pixi.py +5 -5
- fractal_server/tasks/v2/ssh/deactivate.py +7 -7
- fractal_server/tasks/v2/ssh/deactivate_pixi.py +2 -2
- fractal_server/tasks/v2/ssh/delete.py +5 -5
- fractal_server/tasks/v2/ssh/reactivate.py +5 -5
- fractal_server/tasks/v2/ssh/reactivate_pixi.py +5 -5
- fractal_server/tasks/v2/utils_background.py +7 -7
- fractal_server/tasks/v2/utils_database.py +5 -5
- fractal_server/types/__init__.py +22 -0
- fractal_server/types/validators/__init__.py +3 -0
- fractal_server/types/validators/_common_validators.py +32 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/RECORD +108 -98
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,11 +3,10 @@ Auxiliary functions to get object from the database or perform simple checks
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import TypedDict
|
|
7
7
|
|
|
8
8
|
from fastapi import HTTPException
|
|
9
9
|
from fastapi import status
|
|
10
|
-
from sqlalchemy.exc import MultipleResultsFound
|
|
11
10
|
from sqlalchemy.orm.attributes import flag_modified
|
|
12
11
|
from sqlmodel import select
|
|
13
12
|
from sqlmodel.sql.expression import SelectOfScalar
|
|
@@ -23,16 +22,18 @@ from fractal_server.app.models.v2 import ProjectV2
|
|
|
23
22
|
from fractal_server.app.models.v2 import TaskV2
|
|
24
23
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
|
25
24
|
from fractal_server.app.models.v2 import WorkflowV2
|
|
26
|
-
from fractal_server.app.schemas.v2 import
|
|
25
|
+
from fractal_server.app.schemas.v2 import JobStatusType
|
|
26
|
+
from fractal_server.app.schemas.v2 import ProjectPermissions
|
|
27
27
|
from fractal_server.logger import set_logger
|
|
28
28
|
|
|
29
29
|
logger = set_logger(__name__)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
async def
|
|
32
|
+
async def _get_project_check_access(
|
|
33
33
|
*,
|
|
34
34
|
project_id: int,
|
|
35
35
|
user_id: int,
|
|
36
|
+
required_permissions: ProjectPermissions,
|
|
36
37
|
db: AsyncSession,
|
|
37
38
|
) -> ProjectV2:
|
|
38
39
|
"""
|
|
@@ -41,6 +42,7 @@ async def _get_project_check_owner(
|
|
|
41
42
|
Args:
|
|
42
43
|
project_id:
|
|
43
44
|
user_id:
|
|
45
|
+
required_permissions:
|
|
44
46
|
db:
|
|
45
47
|
|
|
46
48
|
Returns:
|
|
@@ -48,31 +50,42 @@ async def _get_project_check_owner(
|
|
|
48
50
|
|
|
49
51
|
Raises:
|
|
50
52
|
HTTPException(status_code=403_FORBIDDEN):
|
|
51
|
-
If the user is not a member of the project
|
|
53
|
+
- If the user is not a member of the project;
|
|
54
|
+
- If the user has not accepted the invitation yet;
|
|
55
|
+
- If the user has not the target permissions.
|
|
52
56
|
HTTPException(status_code=404_NOT_FOUND):
|
|
53
57
|
If the project does not exist
|
|
54
58
|
"""
|
|
55
59
|
project = await db.get(ProjectV2, project_id)
|
|
56
|
-
|
|
57
|
-
link_user_project = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
58
|
-
if not project:
|
|
60
|
+
if project is None:
|
|
59
61
|
raise HTTPException(
|
|
60
62
|
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
|
|
61
63
|
)
|
|
62
|
-
|
|
64
|
+
|
|
65
|
+
link_user_project = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
66
|
+
if (
|
|
67
|
+
link_user_project is None
|
|
68
|
+
or not link_user_project.is_verified
|
|
69
|
+
or required_permissions not in link_user_project.permissions
|
|
70
|
+
):
|
|
63
71
|
raise HTTPException(
|
|
64
72
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
65
|
-
detail=
|
|
73
|
+
detail=(
|
|
74
|
+
"You are not authorized to perform this action. "
|
|
75
|
+
"If you think this is by mistake, "
|
|
76
|
+
"please contact the project owner."
|
|
77
|
+
),
|
|
66
78
|
)
|
|
67
79
|
|
|
68
80
|
return project
|
|
69
81
|
|
|
70
82
|
|
|
71
|
-
async def
|
|
83
|
+
async def _get_workflow_check_access(
|
|
72
84
|
*,
|
|
73
85
|
workflow_id: int,
|
|
74
86
|
project_id: int,
|
|
75
87
|
user_id: int,
|
|
88
|
+
required_permissions: ProjectPermissions,
|
|
76
89
|
db: AsyncSession,
|
|
77
90
|
) -> WorkflowV2:
|
|
78
91
|
"""
|
|
@@ -96,8 +109,11 @@ async def _get_workflow_check_owner(
|
|
|
96
109
|
"""
|
|
97
110
|
|
|
98
111
|
# Access control for project
|
|
99
|
-
await
|
|
100
|
-
project_id=project_id,
|
|
112
|
+
await _get_project_check_access(
|
|
113
|
+
project_id=project_id,
|
|
114
|
+
user_id=user_id,
|
|
115
|
+
required_permissions=required_permissions,
|
|
116
|
+
db=db,
|
|
101
117
|
)
|
|
102
118
|
|
|
103
119
|
res = await db.execute(
|
|
@@ -116,12 +132,13 @@ async def _get_workflow_check_owner(
|
|
|
116
132
|
return workflow
|
|
117
133
|
|
|
118
134
|
|
|
119
|
-
async def
|
|
135
|
+
async def _get_workflow_task_check_access(
|
|
120
136
|
*,
|
|
121
137
|
project_id: int,
|
|
122
138
|
workflow_id: int,
|
|
123
139
|
workflow_task_id: int,
|
|
124
140
|
user_id: int,
|
|
141
|
+
required_permissions: ProjectPermissions,
|
|
125
142
|
db: AsyncSession,
|
|
126
143
|
) -> tuple[WorkflowTaskV2, WorkflowV2]:
|
|
127
144
|
"""
|
|
@@ -146,10 +163,11 @@ async def _get_workflow_task_check_owner(
|
|
|
146
163
|
"""
|
|
147
164
|
|
|
148
165
|
# Access control for workflow
|
|
149
|
-
workflow = await
|
|
166
|
+
workflow = await _get_workflow_check_access(
|
|
150
167
|
workflow_id=workflow_id,
|
|
151
168
|
project_id=project_id,
|
|
152
169
|
user_id=user_id,
|
|
170
|
+
required_permissions=required_permissions,
|
|
153
171
|
db=db,
|
|
154
172
|
)
|
|
155
173
|
|
|
@@ -223,6 +241,7 @@ async def _check_project_exists(
|
|
|
223
241
|
.join(LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id)
|
|
224
242
|
.where(ProjectV2.name == project_name)
|
|
225
243
|
.where(LinkUserProjectV2.user_id == user_id)
|
|
244
|
+
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
226
245
|
)
|
|
227
246
|
res = await db.execute(stm)
|
|
228
247
|
if res.scalars().all():
|
|
@@ -232,13 +251,19 @@ async def _check_project_exists(
|
|
|
232
251
|
)
|
|
233
252
|
|
|
234
253
|
|
|
235
|
-
|
|
254
|
+
class DatasetOrProject(TypedDict):
|
|
255
|
+
dataset: DatasetV2
|
|
256
|
+
project: ProjectV2
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def _get_dataset_check_access(
|
|
236
260
|
*,
|
|
237
261
|
project_id: int,
|
|
238
262
|
dataset_id: int,
|
|
239
263
|
user_id: int,
|
|
264
|
+
required_permissions: ProjectPermissions,
|
|
240
265
|
db: AsyncSession,
|
|
241
|
-
) ->
|
|
266
|
+
) -> DatasetOrProject:
|
|
242
267
|
"""
|
|
243
268
|
Get a dataset and a project, after access control on the project
|
|
244
269
|
|
|
@@ -260,8 +285,11 @@ async def _get_dataset_check_owner(
|
|
|
260
285
|
If the user is not a member of the project
|
|
261
286
|
"""
|
|
262
287
|
# Access control for project
|
|
263
|
-
project = await
|
|
264
|
-
project_id=project_id,
|
|
288
|
+
project = await _get_project_check_access(
|
|
289
|
+
project_id=project_id,
|
|
290
|
+
user_id=user_id,
|
|
291
|
+
required_permissions=required_permissions,
|
|
292
|
+
db=db,
|
|
265
293
|
)
|
|
266
294
|
|
|
267
295
|
res = await db.execute(
|
|
@@ -280,13 +308,19 @@ async def _get_dataset_check_owner(
|
|
|
280
308
|
return dict(dataset=dataset, project=project)
|
|
281
309
|
|
|
282
310
|
|
|
283
|
-
|
|
311
|
+
class JobAndProject(TypedDict):
|
|
312
|
+
job: JobV2
|
|
313
|
+
project: ProjectV2
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def _get_job_check_access(
|
|
284
317
|
*,
|
|
285
318
|
project_id: int,
|
|
286
319
|
job_id: int,
|
|
287
320
|
user_id: int,
|
|
321
|
+
required_permissions: ProjectPermissions,
|
|
288
322
|
db: AsyncSession,
|
|
289
|
-
) ->
|
|
323
|
+
) -> JobAndProject:
|
|
290
324
|
"""
|
|
291
325
|
Get a job and a project, after access control on the project
|
|
292
326
|
|
|
@@ -308,9 +342,10 @@ async def _get_job_check_owner(
|
|
|
308
342
|
If the user is not a member of the project
|
|
309
343
|
"""
|
|
310
344
|
# Access control for project
|
|
311
|
-
project = await
|
|
345
|
+
project = await _get_project_check_access(
|
|
312
346
|
project_id=project_id,
|
|
313
347
|
user_id=user_id,
|
|
348
|
+
required_permissions=required_permissions,
|
|
314
349
|
db=db,
|
|
315
350
|
)
|
|
316
351
|
|
|
@@ -335,7 +370,7 @@ def _get_submitted_jobs_statement() -> SelectOfScalar:
|
|
|
335
370
|
A sqlmodel statement that selects all `Job`s with
|
|
336
371
|
`Job.status` equal to `submitted`.
|
|
337
372
|
"""
|
|
338
|
-
stm = select(JobV2).where(JobV2.status ==
|
|
373
|
+
stm = select(JobV2).where(JobV2.status == JobStatusType.SUBMITTED)
|
|
339
374
|
return stm
|
|
340
375
|
|
|
341
376
|
|
|
@@ -345,7 +380,7 @@ async def _workflow_has_submitted_job(
|
|
|
345
380
|
) -> bool:
|
|
346
381
|
res = await db.execute(
|
|
347
382
|
select(JobV2.id)
|
|
348
|
-
.where(JobV2.status ==
|
|
383
|
+
.where(JobV2.status == JobStatusType.SUBMITTED)
|
|
349
384
|
.where(JobV2.workflow_id == workflow_id)
|
|
350
385
|
.limit(1)
|
|
351
386
|
)
|
|
@@ -427,8 +462,9 @@ async def _workflow_insert_task(
|
|
|
427
462
|
return wf_task
|
|
428
463
|
|
|
429
464
|
|
|
430
|
-
async def
|
|
431
|
-
db: AsyncSession,
|
|
465
|
+
async def clean_app_job_list(
|
|
466
|
+
db: AsyncSession,
|
|
467
|
+
jobs_list: list[int],
|
|
432
468
|
) -> list[int]:
|
|
433
469
|
"""
|
|
434
470
|
Remove from a job list all jobs with status different from submitted.
|
|
@@ -440,14 +476,14 @@ async def clean_app_job_list_v2(
|
|
|
440
476
|
Return:
|
|
441
477
|
List of IDs for submitted jobs.
|
|
442
478
|
"""
|
|
479
|
+
logger.info(f"[clean_app_job_list] START - {jobs_list=}.")
|
|
443
480
|
stmt = select(JobV2).where(JobV2.id.in_(jobs_list))
|
|
444
481
|
result = await db.execute(stmt)
|
|
445
482
|
db_jobs_list = result.scalars().all()
|
|
446
483
|
submitted_job_ids = [
|
|
447
|
-
job.id
|
|
448
|
-
for job in db_jobs_list
|
|
449
|
-
if job.status == JobStatusTypeV2.SUBMITTED
|
|
484
|
+
job.id for job in db_jobs_list if job.status == JobStatusType.SUBMITTED
|
|
450
485
|
]
|
|
486
|
+
logger.info(f"[clean_app_job_list] END - {submitted_job_ids=}.")
|
|
451
487
|
return submitted_job_ids
|
|
452
488
|
|
|
453
489
|
|
|
@@ -517,41 +553,6 @@ async def _get_workflowtask_or_404(
|
|
|
517
553
|
return wftask
|
|
518
554
|
|
|
519
555
|
|
|
520
|
-
async def _get_submitted_job_or_none(
|
|
521
|
-
*,
|
|
522
|
-
dataset_id: int,
|
|
523
|
-
workflow_id: int,
|
|
524
|
-
db: AsyncSession,
|
|
525
|
-
) -> JobV2 | None:
|
|
526
|
-
"""
|
|
527
|
-
Get the submitted job for given dataset/workflow, if any.
|
|
528
|
-
|
|
529
|
-
This function also handles the invalid branch where more than one job
|
|
530
|
-
is found.
|
|
531
|
-
|
|
532
|
-
Args:
|
|
533
|
-
dataset_id:
|
|
534
|
-
workflow_id:
|
|
535
|
-
db:
|
|
536
|
-
"""
|
|
537
|
-
res = await db.execute(
|
|
538
|
-
_get_submitted_jobs_statement()
|
|
539
|
-
.where(JobV2.dataset_id == dataset_id)
|
|
540
|
-
.where(JobV2.workflow_id == workflow_id)
|
|
541
|
-
)
|
|
542
|
-
try:
|
|
543
|
-
return res.scalars().one_or_none()
|
|
544
|
-
except MultipleResultsFound as e:
|
|
545
|
-
error_msg = (
|
|
546
|
-
f"Multiple running jobs found for {dataset_id=} and {workflow_id=}."
|
|
547
|
-
)
|
|
548
|
-
logger.error(f"{error_msg} Original error: {str(e)}.")
|
|
549
|
-
raise HTTPException(
|
|
550
|
-
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
551
|
-
detail=error_msg,
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
|
|
555
556
|
async def _get_user_resource_id(user_id: int, db: AsyncSession) -> int | None:
|
|
556
557
|
res = await db.execute(
|
|
557
558
|
select(Resource.id)
|
|
@@ -13,12 +13,13 @@ from fractal_server.app.models.v2 import HistoryUnit
|
|
|
13
13
|
from fractal_server.app.models.v2 import WorkflowV2
|
|
14
14
|
from fractal_server.app.routes.api.v2._aux_functions import _get_dataset_or_404
|
|
15
15
|
from fractal_server.app.routes.api.v2._aux_functions import (
|
|
16
|
-
|
|
16
|
+
_get_project_check_access,
|
|
17
17
|
)
|
|
18
18
|
from fractal_server.app.routes.api.v2._aux_functions import _get_workflow_or_404
|
|
19
19
|
from fractal_server.app.routes.api.v2._aux_functions import (
|
|
20
20
|
_get_workflowtask_or_404,
|
|
21
21
|
)
|
|
22
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
22
23
|
from fractal_server.logger import set_logger
|
|
23
24
|
from fractal_server.zip_tools import _read_single_file_from_zip
|
|
24
25
|
|
|
@@ -119,6 +120,7 @@ async def _verify_workflow_and_dataset_access(
|
|
|
119
120
|
workflow_id: int,
|
|
120
121
|
dataset_id: int,
|
|
121
122
|
user_id: int,
|
|
123
|
+
required_permissions: ProjectPermissions,
|
|
122
124
|
db: AsyncSession,
|
|
123
125
|
) -> dict[Literal["dataset", "workflow"], DatasetV2 | WorkflowV2]:
|
|
124
126
|
"""
|
|
@@ -131,9 +133,10 @@ async def _verify_workflow_and_dataset_access(
|
|
|
131
133
|
user_id:
|
|
132
134
|
db:
|
|
133
135
|
"""
|
|
134
|
-
await
|
|
136
|
+
await _get_project_check_access(
|
|
135
137
|
project_id=project_id,
|
|
136
138
|
user_id=user_id,
|
|
139
|
+
required_permissions=required_permissions,
|
|
137
140
|
db=db,
|
|
138
141
|
)
|
|
139
142
|
workflow = await _get_workflow_or_404(
|
|
@@ -158,12 +161,13 @@ async def _verify_workflow_and_dataset_access(
|
|
|
158
161
|
return dict(dataset=dataset, workflow=workflow)
|
|
159
162
|
|
|
160
163
|
|
|
161
|
-
async def
|
|
164
|
+
async def get_wftask_check_access(
|
|
162
165
|
*,
|
|
163
166
|
project_id: int,
|
|
164
167
|
dataset_id: int,
|
|
165
168
|
workflowtask_id: int,
|
|
166
169
|
user_id: int,
|
|
170
|
+
required_permissions: ProjectPermissions,
|
|
167
171
|
db: AsyncSession,
|
|
168
172
|
) -> WorkflowTaskV2:
|
|
169
173
|
"""
|
|
@@ -184,6 +188,7 @@ async def get_wftask_check_owner(
|
|
|
184
188
|
project_id=project_id,
|
|
185
189
|
dataset_id=dataset_id,
|
|
186
190
|
workflow_id=wftask.workflow_id,
|
|
191
|
+
required_permissions=required_permissions,
|
|
187
192
|
user_id=user_id,
|
|
188
193
|
db=db,
|
|
189
194
|
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from fastapi import status
|
|
3
|
+
from sqlmodel import select
|
|
4
|
+
|
|
5
|
+
from fractal_server.app.db import AsyncSession
|
|
6
|
+
from fractal_server.app.models import UserOAuth
|
|
7
|
+
from fractal_server.app.models.v2 import LinkUserProjectV2
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def raise_403_if_not_owner(
|
|
11
|
+
*,
|
|
12
|
+
user_id: int,
|
|
13
|
+
project_id: int,
|
|
14
|
+
db: AsyncSession,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Raises 403 if User[`user_id`] is not owner of Project[`project_id`],
|
|
18
|
+
regardless of whether the User or Project exists.
|
|
19
|
+
"""
|
|
20
|
+
res = await db.execute(
|
|
21
|
+
select(LinkUserProjectV2)
|
|
22
|
+
.where(LinkUserProjectV2.project_id == project_id)
|
|
23
|
+
.where(LinkUserProjectV2.user_id == user_id)
|
|
24
|
+
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
25
|
+
)
|
|
26
|
+
link = res.scalars().one_or_none()
|
|
27
|
+
if link is None:
|
|
28
|
+
raise HTTPException(
|
|
29
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
30
|
+
detail="Current user is not the project owner.",
|
|
31
|
+
)
|
|
32
|
+
return link
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def get_link_or_404(
|
|
36
|
+
*, user_id: int, project_id: int, db: AsyncSession
|
|
37
|
+
) -> LinkUserProjectV2:
|
|
38
|
+
"""
|
|
39
|
+
Raises 404 if User[`user_id`] is not linked to Project[`project_id`],
|
|
40
|
+
regardless of whether the User or Project exists.
|
|
41
|
+
"""
|
|
42
|
+
link = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
43
|
+
if link is None:
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
46
|
+
detail="User is not linked to project.",
|
|
47
|
+
)
|
|
48
|
+
return link
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def get_pending_invitation_or_404(
|
|
52
|
+
*, user_id: int, project_id: int, db: AsyncSession
|
|
53
|
+
) -> LinkUserProjectV2:
|
|
54
|
+
"""
|
|
55
|
+
Raises 404 if User[`user_id`] has not a pending invitation to
|
|
56
|
+
Project[`project_id`], regardless of whether the User or Project exists.
|
|
57
|
+
"""
|
|
58
|
+
link = await get_link_or_404(user_id=user_id, project_id=project_id, db=db)
|
|
59
|
+
if link.is_verified:
|
|
60
|
+
raise HTTPException(
|
|
61
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
62
|
+
detail="No pending invitation for user on this project.",
|
|
63
|
+
)
|
|
64
|
+
return link
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def raise_422_if_link_exists(
|
|
68
|
+
*, user_id: int, project_id: int, db: AsyncSession
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Raises 422 if User[`user_id`] is linked Project[`project_id`], regardless
|
|
72
|
+
of whether the User or Project exists.
|
|
73
|
+
"""
|
|
74
|
+
link = await db.get(LinkUserProjectV2, (project_id, user_id))
|
|
75
|
+
if link is not None:
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
78
|
+
detail="User is already associated to project.",
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def get_user_id_from_email_or_404(
|
|
84
|
+
*, user_email: str, db: AsyncSession
|
|
85
|
+
) -> int:
|
|
86
|
+
"""
|
|
87
|
+
Raises 404 if there is no User with email `user_email`.
|
|
88
|
+
"""
|
|
89
|
+
res = await db.execute(
|
|
90
|
+
select(UserOAuth.id).where(UserOAuth.email == user_email)
|
|
91
|
+
)
|
|
92
|
+
user_id = res.scalar_one_or_none()
|
|
93
|
+
if user_id is None:
|
|
94
|
+
raise HTTPException(
|
|
95
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
|
|
96
|
+
)
|
|
97
|
+
return user_id
|
|
@@ -14,8 +14,8 @@ from fractal_server.app.models.v2 import TaskGroupV2
|
|
|
14
14
|
from fractal_server.app.models.v2 import TaskV2
|
|
15
15
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
|
16
16
|
from fractal_server.app.models.v2 import WorkflowV2
|
|
17
|
-
from fractal_server.app.schemas.v2 import
|
|
18
|
-
from fractal_server.app.schemas.v2 import
|
|
17
|
+
from fractal_server.app.schemas.v2 import JobStatusType
|
|
18
|
+
from fractal_server.app.schemas.v2 import TaskGroupActivityStatus
|
|
19
19
|
from fractal_server.logger import set_logger
|
|
20
20
|
from fractal_server.tasks.v2.utils_package_names import normalize_package_name
|
|
21
21
|
|
|
@@ -171,7 +171,7 @@ async def check_no_ongoing_activity(
|
|
|
171
171
|
stm = (
|
|
172
172
|
select(TaskGroupActivityV2)
|
|
173
173
|
.where(TaskGroupActivityV2.taskgroupv2_id == task_group_id)
|
|
174
|
-
.where(TaskGroupActivityV2.status ==
|
|
174
|
+
.where(TaskGroupActivityV2.status == TaskGroupActivityStatus.ONGOING)
|
|
175
175
|
)
|
|
176
176
|
res = await db.execute(stm)
|
|
177
177
|
ongoing_activities = res.scalars().all()
|
|
@@ -213,7 +213,7 @@ async def check_no_submitted_job(
|
|
|
213
213
|
.join(TaskV2, WorkflowTaskV2.task_id == TaskV2.id)
|
|
214
214
|
.where(WorkflowTaskV2.order >= JobV2.first_task_index)
|
|
215
215
|
.where(WorkflowTaskV2.order <= JobV2.last_task_index)
|
|
216
|
-
.where(JobV2.status ==
|
|
216
|
+
.where(JobV2.status == JobStatusType.SUBMITTED)
|
|
217
217
|
.where(TaskV2.taskgroupv2_id == task_group_id)
|
|
218
218
|
)
|
|
219
219
|
res = await db.execute(stm)
|
|
@@ -27,7 +27,7 @@ from fractal_server.app.routes.auth._aux_auth import (
|
|
|
27
27
|
from fractal_server.app.routes.auth._aux_auth import (
|
|
28
28
|
_verify_user_belongs_to_group,
|
|
29
29
|
)
|
|
30
|
-
from fractal_server.app.schemas.v2 import
|
|
30
|
+
from fractal_server.app.schemas.v2 import TaskGroupActivityAction
|
|
31
31
|
from fractal_server.images.tools import merge_type_filters
|
|
32
32
|
from fractal_server.logger import set_logger
|
|
33
33
|
|
|
@@ -252,7 +252,7 @@ async def _get_collection_task_group_activity_status_message(
|
|
|
252
252
|
res = await db.execute(
|
|
253
253
|
select(TaskGroupActivityV2)
|
|
254
254
|
.where(TaskGroupActivityV2.taskgroupv2_id == task_group_id)
|
|
255
|
-
.where(TaskGroupActivityV2.action ==
|
|
255
|
+
.where(TaskGroupActivityV2.action == TaskGroupActivityAction.COLLECT)
|
|
256
256
|
)
|
|
257
257
|
task_group_activity_list = res.scalars().all()
|
|
258
258
|
if len(task_group_activity_list) > 1:
|