fractal-server 2.17.2__py3-none-any.whl → 2.18.0a0__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/app/models/linkuserproject.py +40 -0
- fractal_server/app/routes/admin/v2/__init__.py +2 -0
- fractal_server/app/routes/admin/v2/job.py +17 -6
- fractal_server/app/routes/admin/v2/sharing.py +103 -0
- fractal_server/app/routes/admin/v2/task.py +1 -0
- fractal_server/app/routes/api/v2/__init__.py +2 -0
- fractal_server/app/routes/api/v2/_aux_functions.py +43 -17
- 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/dataset.py +23 -17
- fractal_server/app/routes/api/v2/history.py +21 -11
- fractal_server/app/routes/api/v2/images.py +22 -8
- fractal_server/app/routes/api/v2/job.py +28 -12
- fractal_server/app/routes/api/v2/pre_submission_checks.py +13 -6
- fractal_server/app/routes/api/v2/project.py +37 -14
- fractal_server/app/routes/api/v2/sharing.py +312 -0
- fractal_server/app/routes/api/v2/status_legacy.py +7 -4
- fractal_server/app/routes/api/v2/submit.py +11 -5
- fractal_server/app/routes/api/v2/task_version_update.py +7 -4
- fractal_server/app/routes/api/v2/workflow.py +23 -11
- fractal_server/app/routes/api/v2/workflow_import.py +14 -12
- fractal_server/app/routes/api/v2/workflowtask.py +41 -7
- fractal_server/app/schemas/v2/__init__.py +7 -0
- fractal_server/app/schemas/v2/sharing.py +99 -0
- fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
- fractal_server/runner/executors/slurm_common/_batching.py +4 -10
- fractal_server/runner/executors/slurm_ssh/runner.py +1 -1
- fractal_server/runner/executors/slurm_sudo/runner.py +1 -1
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0a0.dist-info}/METADATA +3 -2
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0a0.dist-info}/RECORD +34 -29
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0a0.dist-info}/WHEEL +0 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0a0.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.17.2.dist-info → fractal_server-2.18.0a0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from fastapi import Depends
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
from fastapi import Response
|
|
5
|
+
from fastapi import status
|
|
6
|
+
from pydantic import EmailStr
|
|
7
|
+
from sqlmodel import select
|
|
8
|
+
|
|
9
|
+
from fractal_server.app.db import AsyncSession
|
|
10
|
+
from fractal_server.app.db import get_async_db
|
|
11
|
+
from fractal_server.app.models import UserOAuth
|
|
12
|
+
from fractal_server.app.models.v2 import LinkUserProjectV2
|
|
13
|
+
from fractal_server.app.models.v2 import ProjectV2
|
|
14
|
+
from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
15
|
+
from fractal_server.app.schemas.v2 import ProjectAccessRead
|
|
16
|
+
from fractal_server.app.schemas.v2 import ProjectGuestCreate
|
|
17
|
+
from fractal_server.app.schemas.v2 import ProjectGuestRead
|
|
18
|
+
from fractal_server.app.schemas.v2 import ProjectGuestUpdate
|
|
19
|
+
from fractal_server.app.schemas.v2 import ProjectInvitationRead
|
|
20
|
+
|
|
21
|
+
from ._aux_functions_sharing import get_link_or_404
|
|
22
|
+
from ._aux_functions_sharing import get_pending_invitation_or_404
|
|
23
|
+
from ._aux_functions_sharing import get_user_id_from_email_or_404
|
|
24
|
+
from ._aux_functions_sharing import raise_403_if_not_owner
|
|
25
|
+
from ._aux_functions_sharing import raise_422_if_link_exists
|
|
26
|
+
|
|
27
|
+
router = APIRouter()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get(
|
|
31
|
+
"/project/{project_id}/guest/",
|
|
32
|
+
response_model=list[ProjectGuestRead],
|
|
33
|
+
)
|
|
34
|
+
async def get_project_guests(
|
|
35
|
+
project_id: int,
|
|
36
|
+
owner: UserOAuth = Depends(current_user_act_ver_prof),
|
|
37
|
+
db: AsyncSession = Depends(get_async_db),
|
|
38
|
+
) -> list[ProjectGuestRead]:
|
|
39
|
+
"""
|
|
40
|
+
Get the list of all the guests of your project (verified or not).
|
|
41
|
+
"""
|
|
42
|
+
await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
|
|
43
|
+
# Get (email, is_verified, permissions) for all guests
|
|
44
|
+
res = await db.execute(
|
|
45
|
+
select(
|
|
46
|
+
UserOAuth.email,
|
|
47
|
+
LinkUserProjectV2.is_verified,
|
|
48
|
+
LinkUserProjectV2.permissions,
|
|
49
|
+
)
|
|
50
|
+
.join(LinkUserProjectV2, LinkUserProjectV2.user_id == UserOAuth.id)
|
|
51
|
+
.where(LinkUserProjectV2.project_id == project_id)
|
|
52
|
+
.where(LinkUserProjectV2.is_owner.is_(False))
|
|
53
|
+
.order_by(UserOAuth.email)
|
|
54
|
+
)
|
|
55
|
+
guest_tuples = res.all()
|
|
56
|
+
return [
|
|
57
|
+
dict(
|
|
58
|
+
email=guest_email,
|
|
59
|
+
is_verified=is_verified,
|
|
60
|
+
permissions=permissions,
|
|
61
|
+
)
|
|
62
|
+
for guest_email, is_verified, permissions in guest_tuples
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.post("/project/{project_id}/guest/", status_code=201)
|
|
67
|
+
async def invite_guest(
|
|
68
|
+
project_id: int,
|
|
69
|
+
email: EmailStr,
|
|
70
|
+
project_invitation: ProjectGuestCreate,
|
|
71
|
+
owner: UserOAuth = Depends(current_user_act_ver_prof),
|
|
72
|
+
db: AsyncSession = Depends(get_async_db),
|
|
73
|
+
) -> Response:
|
|
74
|
+
"""
|
|
75
|
+
Add a guest to your project.
|
|
76
|
+
"""
|
|
77
|
+
await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
|
|
78
|
+
|
|
79
|
+
guest_id = await get_user_id_from_email_or_404(user_email=email, db=db)
|
|
80
|
+
|
|
81
|
+
await raise_422_if_link_exists(
|
|
82
|
+
user_id=guest_id,
|
|
83
|
+
project_id=project_id,
|
|
84
|
+
db=db,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
db.add(
|
|
88
|
+
LinkUserProjectV2(
|
|
89
|
+
project_id=project_id,
|
|
90
|
+
user_id=guest_id,
|
|
91
|
+
is_owner=False,
|
|
92
|
+
is_verified=False,
|
|
93
|
+
permissions=project_invitation.permissions,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
await db.commit()
|
|
97
|
+
|
|
98
|
+
return Response(status_code=status.HTTP_201_CREATED)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.patch("/project/{project_id}/guest/", status_code=200)
|
|
102
|
+
async def patch_guest(
|
|
103
|
+
project_id: int,
|
|
104
|
+
email: EmailStr,
|
|
105
|
+
update: ProjectGuestUpdate,
|
|
106
|
+
owner: UserOAuth = Depends(current_user_act_ver_prof),
|
|
107
|
+
db: AsyncSession = Depends(get_async_db),
|
|
108
|
+
) -> Response:
|
|
109
|
+
"""
|
|
110
|
+
Change guest's permissions on your project.
|
|
111
|
+
"""
|
|
112
|
+
await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
|
|
113
|
+
|
|
114
|
+
guest_id = await get_user_id_from_email_or_404(user_email=email, db=db)
|
|
115
|
+
|
|
116
|
+
if guest_id == owner.id:
|
|
117
|
+
raise HTTPException(
|
|
118
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
119
|
+
detail="Cannot perform this operation on project owner.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
link = await get_link_or_404(
|
|
123
|
+
user_id=guest_id,
|
|
124
|
+
project_id=project_id,
|
|
125
|
+
db=db,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Update link and commit
|
|
129
|
+
for key, value in update.model_dump(exclude_unset=True).items():
|
|
130
|
+
setattr(link, key, value)
|
|
131
|
+
await db.commit()
|
|
132
|
+
|
|
133
|
+
return Response(status_code=status.HTTP_200_OK)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@router.delete("/project/{project_id}/guest/", status_code=204)
|
|
137
|
+
async def revoke_guest_access(
|
|
138
|
+
project_id: int,
|
|
139
|
+
email: EmailStr,
|
|
140
|
+
owner: UserOAuth = Depends(current_user_act_ver_prof),
|
|
141
|
+
db: AsyncSession = Depends(get_async_db),
|
|
142
|
+
) -> Response:
|
|
143
|
+
"""
|
|
144
|
+
Remove a guest from your project.
|
|
145
|
+
"""
|
|
146
|
+
await raise_403_if_not_owner(user_id=owner.id, project_id=project_id, db=db)
|
|
147
|
+
|
|
148
|
+
guest_id = await get_user_id_from_email_or_404(user_email=email, db=db)
|
|
149
|
+
|
|
150
|
+
if guest_id == owner.id:
|
|
151
|
+
raise HTTPException(
|
|
152
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
153
|
+
detail="Cannot perform this operation on project owner.",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
link = await get_link_or_404(
|
|
157
|
+
user_id=guest_id,
|
|
158
|
+
project_id=project_id,
|
|
159
|
+
db=db,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Delete link and commit
|
|
163
|
+
await db.delete(link)
|
|
164
|
+
await db.commit()
|
|
165
|
+
|
|
166
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@router.get(
|
|
170
|
+
"/project/invitation/",
|
|
171
|
+
response_model=list[ProjectInvitationRead],
|
|
172
|
+
)
|
|
173
|
+
async def get_pending_invitations(
|
|
174
|
+
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
175
|
+
db: AsyncSession = Depends(get_async_db),
|
|
176
|
+
) -> list[ProjectInvitationRead]:
|
|
177
|
+
"""
|
|
178
|
+
See your current invitations.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
res = await db.execute(
|
|
182
|
+
select(
|
|
183
|
+
ProjectV2.id,
|
|
184
|
+
ProjectV2.name,
|
|
185
|
+
LinkUserProjectV2.permissions,
|
|
186
|
+
(
|
|
187
|
+
select(UserOAuth.email)
|
|
188
|
+
.join(
|
|
189
|
+
LinkUserProjectV2,
|
|
190
|
+
UserOAuth.id == LinkUserProjectV2.user_id,
|
|
191
|
+
)
|
|
192
|
+
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
193
|
+
.where(LinkUserProjectV2.project_id == ProjectV2.id)
|
|
194
|
+
.scalar_subquery()
|
|
195
|
+
.correlate(ProjectV2)
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
.join(LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id)
|
|
199
|
+
.where(LinkUserProjectV2.user_id == user.id)
|
|
200
|
+
.where(LinkUserProjectV2.is_verified.is_(False))
|
|
201
|
+
.order_by(ProjectV2.name)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
guest_project_info = res.all()
|
|
205
|
+
|
|
206
|
+
return [
|
|
207
|
+
dict(
|
|
208
|
+
project_id=project_id,
|
|
209
|
+
project_name=project_name,
|
|
210
|
+
guest_permissions=guest_permissions,
|
|
211
|
+
owner_email=owner_email,
|
|
212
|
+
)
|
|
213
|
+
for (
|
|
214
|
+
project_id,
|
|
215
|
+
project_name,
|
|
216
|
+
guest_permissions,
|
|
217
|
+
owner_email,
|
|
218
|
+
) in guest_project_info
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@router.get(
|
|
223
|
+
"/project/{project_id}/access/",
|
|
224
|
+
response_model=ProjectAccessRead,
|
|
225
|
+
)
|
|
226
|
+
async def get_access_info(
|
|
227
|
+
project_id: int,
|
|
228
|
+
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
229
|
+
db: AsyncSession = Depends(get_async_db),
|
|
230
|
+
) -> ProjectAccessRead:
|
|
231
|
+
"""
|
|
232
|
+
Returns information on your relationship with Project[`project_id`].
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
res = await db.execute(
|
|
236
|
+
select(
|
|
237
|
+
LinkUserProjectV2.is_owner,
|
|
238
|
+
LinkUserProjectV2.permissions,
|
|
239
|
+
(
|
|
240
|
+
select(UserOAuth.email)
|
|
241
|
+
.join(
|
|
242
|
+
LinkUserProjectV2,
|
|
243
|
+
UserOAuth.id == LinkUserProjectV2.user_id,
|
|
244
|
+
)
|
|
245
|
+
.where(LinkUserProjectV2.is_owner.is_(True))
|
|
246
|
+
.where(LinkUserProjectV2.project_id == ProjectV2.id)
|
|
247
|
+
.scalar_subquery()
|
|
248
|
+
.correlate(LinkUserProjectV2)
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
.where(LinkUserProjectV2.project_id == project_id)
|
|
252
|
+
.where(LinkUserProjectV2.user_id == user.id)
|
|
253
|
+
.where(LinkUserProjectV2.is_verified.is_(True))
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
guest_project_info = res.one_or_none()
|
|
257
|
+
|
|
258
|
+
if guest_project_info is None:
|
|
259
|
+
raise HTTPException(
|
|
260
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
261
|
+
detail=f"User has no access to project {project_id}.",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
is_owner, permissions, owner_email = guest_project_info
|
|
265
|
+
|
|
266
|
+
return dict(
|
|
267
|
+
is_owner=is_owner,
|
|
268
|
+
permissions=permissions,
|
|
269
|
+
owner_email=owner_email,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@router.post("/project/{project_id}/access/accept/", status_code=200)
|
|
274
|
+
async def accept_project_invitation(
|
|
275
|
+
project_id: int,
|
|
276
|
+
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
277
|
+
db: AsyncSession = Depends(get_async_db),
|
|
278
|
+
) -> Response:
|
|
279
|
+
"""
|
|
280
|
+
Accept invitation to project `project_id`.
|
|
281
|
+
"""
|
|
282
|
+
link = await get_pending_invitation_or_404(
|
|
283
|
+
user_id=user.id, project_id=project_id, db=db
|
|
284
|
+
)
|
|
285
|
+
link.is_verified = True
|
|
286
|
+
await db.commit()
|
|
287
|
+
|
|
288
|
+
return Response(status_code=status.HTTP_200_OK)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@router.delete("/project/{project_id}/access/", status_code=204)
|
|
292
|
+
async def leave_project(
|
|
293
|
+
project_id: int,
|
|
294
|
+
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
295
|
+
db: AsyncSession = Depends(get_async_db),
|
|
296
|
+
) -> Response:
|
|
297
|
+
"""
|
|
298
|
+
Decline invitation to project `project_id` or stop being a guest of that
|
|
299
|
+
project.
|
|
300
|
+
"""
|
|
301
|
+
link = await get_link_or_404(user_id=user.id, project_id=project_id, db=db)
|
|
302
|
+
|
|
303
|
+
if link.is_owner:
|
|
304
|
+
raise HTTPException(
|
|
305
|
+
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
306
|
+
detail=f"You are the owner of project {project_id}.",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
await db.delete(link)
|
|
310
|
+
await db.commit()
|
|
311
|
+
|
|
312
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
@@ -8,13 +8,14 @@ from fractal_server.app.db import get_async_db
|
|
|
8
8
|
from fractal_server.app.models import UserOAuth
|
|
9
9
|
from fractal_server.app.models.v2 import JobV2
|
|
10
10
|
from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
11
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
11
12
|
from fractal_server.app.schemas.v2.status_legacy import LegacyStatusReadV2
|
|
12
13
|
from fractal_server.app.schemas.v2.status_legacy import WorkflowTaskStatusTypeV2
|
|
13
14
|
from fractal_server.logger import set_logger
|
|
14
15
|
|
|
15
|
-
from ._aux_functions import
|
|
16
|
+
from ._aux_functions import _get_dataset_check_access
|
|
16
17
|
from ._aux_functions import _get_submitted_jobs_statement
|
|
17
|
-
from ._aux_functions import
|
|
18
|
+
from ._aux_functions import _get_workflow_check_access
|
|
18
19
|
|
|
19
20
|
router = APIRouter()
|
|
20
21
|
|
|
@@ -42,19 +43,21 @@ async def get_workflowtask_status(
|
|
|
42
43
|
order). See fractal-server GitHub issues: 793, 1083.
|
|
43
44
|
"""
|
|
44
45
|
# Get the dataset DB entry
|
|
45
|
-
output = await
|
|
46
|
+
output = await _get_dataset_check_access(
|
|
46
47
|
project_id=project_id,
|
|
47
48
|
dataset_id=dataset_id,
|
|
48
49
|
user_id=user.id,
|
|
50
|
+
required_permissions=ProjectPermissions.READ,
|
|
49
51
|
db=db,
|
|
50
52
|
)
|
|
51
53
|
dataset = output["dataset"]
|
|
52
54
|
|
|
53
55
|
# Get the workflow DB entry
|
|
54
|
-
workflow = await
|
|
56
|
+
workflow = await _get_workflow_check_access(
|
|
55
57
|
project_id=project_id,
|
|
56
58
|
workflow_id=workflow_id,
|
|
57
59
|
user_id=user.id,
|
|
60
|
+
required_permissions=ProjectPermissions.READ,
|
|
58
61
|
db=db,
|
|
59
62
|
)
|
|
60
63
|
|
|
@@ -27,6 +27,7 @@ from fractal_server.app.schemas.v2 import JobCreateV2
|
|
|
27
27
|
from fractal_server.app.schemas.v2 import JobReadV2
|
|
28
28
|
from fractal_server.app.schemas.v2 import JobStatusTypeV2
|
|
29
29
|
from fractal_server.app.schemas.v2 import ResourceType
|
|
30
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
30
31
|
from fractal_server.config import get_settings
|
|
31
32
|
from fractal_server.logger import set_logger
|
|
32
33
|
from fractal_server.runner.set_start_and_last_task_index import (
|
|
@@ -35,8 +36,8 @@ from fractal_server.runner.set_start_and_last_task_index import (
|
|
|
35
36
|
from fractal_server.runner.v2.submit_workflow import submit_workflow
|
|
36
37
|
from fractal_server.syringe import Inject
|
|
37
38
|
|
|
38
|
-
from ._aux_functions import
|
|
39
|
-
from ._aux_functions import
|
|
39
|
+
from ._aux_functions import _get_dataset_check_access
|
|
40
|
+
from ._aux_functions import _get_workflow_check_access
|
|
40
41
|
from ._aux_functions import clean_app_job_list_v2
|
|
41
42
|
from ._aux_functions_tasks import _check_type_filters_compatibility
|
|
42
43
|
|
|
@@ -72,10 +73,11 @@ async def apply_workflow(
|
|
|
72
73
|
)
|
|
73
74
|
request.app.state.jobsV2 = new_jobs_list
|
|
74
75
|
|
|
75
|
-
output = await
|
|
76
|
+
output = await _get_dataset_check_access(
|
|
76
77
|
project_id=project_id,
|
|
77
78
|
dataset_id=dataset_id,
|
|
78
79
|
user_id=user.id,
|
|
80
|
+
required_permissions=ProjectPermissions.EXECUTE,
|
|
79
81
|
db=db,
|
|
80
82
|
)
|
|
81
83
|
project = output["project"]
|
|
@@ -92,8 +94,12 @@ async def apply_workflow(
|
|
|
92
94
|
detail="Project resource does not match with user's resource",
|
|
93
95
|
)
|
|
94
96
|
|
|
95
|
-
workflow = await
|
|
96
|
-
project_id=project_id,
|
|
97
|
+
workflow = await _get_workflow_check_access(
|
|
98
|
+
project_id=project_id,
|
|
99
|
+
workflow_id=workflow_id,
|
|
100
|
+
user_id=user.id,
|
|
101
|
+
required_permissions=ProjectPermissions.EXECUTE,
|
|
102
|
+
db=db,
|
|
97
103
|
)
|
|
98
104
|
num_tasks = len(workflow.task_list)
|
|
99
105
|
if num_tasks == 0:
|
|
@@ -21,9 +21,10 @@ from fractal_server.app.routes.auth import current_user_act_ver_prof
|
|
|
21
21
|
from fractal_server.app.schemas.v2 import TaskType
|
|
22
22
|
from fractal_server.app.schemas.v2 import WorkflowTaskReadV2
|
|
23
23
|
from fractal_server.app.schemas.v2 import WorkflowTaskReplaceV2
|
|
24
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
24
25
|
|
|
25
|
-
from ._aux_functions import
|
|
26
|
-
from ._aux_functions import
|
|
26
|
+
from ._aux_functions import _get_workflow_check_access
|
|
27
|
+
from ._aux_functions import _get_workflow_task_check_access
|
|
27
28
|
from ._aux_functions_task_version_update import get_new_workflow_task_meta
|
|
28
29
|
from ._aux_functions_tasks import _check_type_filters_compatibility
|
|
29
30
|
from ._aux_functions_tasks import _get_task_group_or_404
|
|
@@ -79,10 +80,11 @@ async def get_workflow_version_update_candidates(
|
|
|
79
80
|
user: UserOAuth = Depends(current_user_act_ver_prof),
|
|
80
81
|
db: AsyncSession = Depends(get_async_db),
|
|
81
82
|
) -> list[list[TaskVersionRead]]:
|
|
82
|
-
workflow = await
|
|
83
|
+
workflow = await _get_workflow_check_access(
|
|
83
84
|
project_id=project_id,
|
|
84
85
|
workflow_id=workflow_id,
|
|
85
86
|
user_id=user.id,
|
|
87
|
+
required_permissions=ProjectPermissions.READ,
|
|
86
88
|
db=db,
|
|
87
89
|
)
|
|
88
90
|
|
|
@@ -182,11 +184,12 @@ async def replace_workflowtask(
|
|
|
182
184
|
db: AsyncSession = Depends(get_async_db),
|
|
183
185
|
) -> WorkflowTaskReadV2:
|
|
184
186
|
# Get objects from database
|
|
185
|
-
workflow_task, workflow = await
|
|
187
|
+
workflow_task, workflow = await _get_workflow_task_check_access(
|
|
186
188
|
project_id=project_id,
|
|
187
189
|
workflow_id=workflow_id,
|
|
188
190
|
workflow_task_id=workflow_task_id,
|
|
189
191
|
user_id=user.id,
|
|
192
|
+
required_permissions=ProjectPermissions.WRITE,
|
|
190
193
|
db=db,
|
|
191
194
|
)
|
|
192
195
|
new_task = await _get_task_read_access(
|
|
@@ -20,12 +20,13 @@ from fractal_server.app.schemas.v2 import WorkflowExportV2
|
|
|
20
20
|
from fractal_server.app.schemas.v2 import WorkflowReadV2
|
|
21
21
|
from fractal_server.app.schemas.v2 import WorkflowReadV2WithWarnings
|
|
22
22
|
from fractal_server.app.schemas.v2 import WorkflowUpdateV2
|
|
23
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
23
24
|
from fractal_server.images.tools import merge_type_filters
|
|
24
25
|
|
|
25
26
|
from ._aux_functions import _check_workflow_exists
|
|
26
|
-
from ._aux_functions import
|
|
27
|
+
from ._aux_functions import _get_project_check_access
|
|
27
28
|
from ._aux_functions import _get_submitted_jobs_statement
|
|
28
|
-
from ._aux_functions import
|
|
29
|
+
from ._aux_functions import _get_workflow_check_access
|
|
29
30
|
from ._aux_functions import _workflow_has_submitted_job
|
|
30
31
|
from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
|
|
31
32
|
|
|
@@ -45,8 +46,11 @@ async def get_workflow_list(
|
|
|
45
46
|
Get workflow list for given project
|
|
46
47
|
"""
|
|
47
48
|
# Access control
|
|
48
|
-
project = await
|
|
49
|
-
project_id=project_id,
|
|
49
|
+
project = await _get_project_check_access(
|
|
50
|
+
project_id=project_id,
|
|
51
|
+
user_id=user.id,
|
|
52
|
+
required_permissions=ProjectPermissions.READ,
|
|
53
|
+
db=db,
|
|
50
54
|
)
|
|
51
55
|
# Find workflows of the current project. Note: this select/where approach
|
|
52
56
|
# has much better scaling than refreshing all elements of
|
|
@@ -71,8 +75,11 @@ async def create_workflow(
|
|
|
71
75
|
"""
|
|
72
76
|
Create a workflow, associate to a project
|
|
73
77
|
"""
|
|
74
|
-
await
|
|
75
|
-
project_id=project_id,
|
|
78
|
+
await _get_project_check_access(
|
|
79
|
+
project_id=project_id,
|
|
80
|
+
user_id=user.id,
|
|
81
|
+
required_permissions=ProjectPermissions.WRITE,
|
|
82
|
+
db=db,
|
|
76
83
|
)
|
|
77
84
|
await _check_workflow_exists(
|
|
78
85
|
name=workflow.name, project_id=project_id, db=db
|
|
@@ -100,10 +107,11 @@ async def read_workflow(
|
|
|
100
107
|
Get info on an existing workflow
|
|
101
108
|
"""
|
|
102
109
|
|
|
103
|
-
workflow = await
|
|
110
|
+
workflow = await _get_workflow_check_access(
|
|
104
111
|
project_id=project_id,
|
|
105
112
|
workflow_id=workflow_id,
|
|
106
113
|
user_id=user.id,
|
|
114
|
+
required_permissions=ProjectPermissions.READ,
|
|
107
115
|
db=db,
|
|
108
116
|
)
|
|
109
117
|
|
|
@@ -133,10 +141,11 @@ async def update_workflow(
|
|
|
133
141
|
"""
|
|
134
142
|
Edit a workflow
|
|
135
143
|
"""
|
|
136
|
-
workflow = await
|
|
144
|
+
workflow = await _get_workflow_check_access(
|
|
137
145
|
project_id=project_id,
|
|
138
146
|
workflow_id=workflow_id,
|
|
139
147
|
user_id=user.id,
|
|
148
|
+
required_permissions=ProjectPermissions.WRITE,
|
|
140
149
|
db=db,
|
|
141
150
|
)
|
|
142
151
|
|
|
@@ -208,10 +217,11 @@ async def delete_workflow(
|
|
|
208
217
|
Delete a workflow
|
|
209
218
|
"""
|
|
210
219
|
|
|
211
|
-
workflow = await
|
|
220
|
+
workflow = await _get_workflow_check_access(
|
|
212
221
|
project_id=project_id,
|
|
213
222
|
workflow_id=workflow_id,
|
|
214
223
|
user_id=user.id,
|
|
224
|
+
required_permissions=ProjectPermissions.WRITE,
|
|
215
225
|
db=db,
|
|
216
226
|
)
|
|
217
227
|
|
|
@@ -252,10 +262,11 @@ async def export_workflow(
|
|
|
252
262
|
"""
|
|
253
263
|
Export an existing workflow, after stripping all IDs
|
|
254
264
|
"""
|
|
255
|
-
workflow = await
|
|
265
|
+
workflow = await _get_workflow_check_access(
|
|
256
266
|
project_id=project_id,
|
|
257
267
|
workflow_id=workflow_id,
|
|
258
268
|
user_id=user.id,
|
|
269
|
+
required_permissions=ProjectPermissions.READ,
|
|
259
270
|
db=db,
|
|
260
271
|
)
|
|
261
272
|
wf_task_list = []
|
|
@@ -293,10 +304,11 @@ async def get_workflow_type_filters(
|
|
|
293
304
|
Get info on type/type-filters flow for a workflow.
|
|
294
305
|
"""
|
|
295
306
|
|
|
296
|
-
workflow = await
|
|
307
|
+
workflow = await _get_workflow_check_access(
|
|
297
308
|
project_id=project_id,
|
|
298
309
|
workflow_id=workflow_id,
|
|
299
310
|
user_id=user.id,
|
|
311
|
+
required_permissions=ProjectPermissions.READ,
|
|
300
312
|
db=db,
|
|
301
313
|
)
|
|
302
314
|
|
|
@@ -24,10 +24,11 @@ from fractal_server.app.schemas.v2 import TaskImportV2Legacy
|
|
|
24
24
|
from fractal_server.app.schemas.v2 import WorkflowImportV2
|
|
25
25
|
from fractal_server.app.schemas.v2 import WorkflowReadV2WithWarnings
|
|
26
26
|
from fractal_server.app.schemas.v2 import WorkflowTaskCreateV2
|
|
27
|
+
from fractal_server.app.schemas.v2.sharing import ProjectPermissions
|
|
27
28
|
from fractal_server.logger import set_logger
|
|
28
29
|
|
|
29
30
|
from ._aux_functions import _check_workflow_exists
|
|
30
|
-
from ._aux_functions import
|
|
31
|
+
from ._aux_functions import _get_project_check_access
|
|
31
32
|
from ._aux_functions import _get_user_resource_id
|
|
32
33
|
from ._aux_functions import _workflow_insert_task
|
|
33
34
|
from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
|
|
@@ -65,7 +66,7 @@ async def _get_user_accessible_taskgroups(
|
|
|
65
66
|
)
|
|
66
67
|
res = await db.execute(stm)
|
|
67
68
|
accessible_task_groups = res.scalars().all()
|
|
68
|
-
logger.
|
|
69
|
+
logger.debug(
|
|
69
70
|
f"Found {len(accessible_task_groups)} accessible "
|
|
70
71
|
f"task groups for {user_id=}."
|
|
71
72
|
)
|
|
@@ -120,7 +121,7 @@ async def _get_task_by_taskimport(
|
|
|
120
121
|
`id` of the matching task, or `None`.
|
|
121
122
|
"""
|
|
122
123
|
|
|
123
|
-
logger.
|
|
124
|
+
logger.debug(f"[_get_task_by_taskimport] START, {task_import=}")
|
|
124
125
|
|
|
125
126
|
# Filter by `pkg_name` and by presence of a task with given `name`.
|
|
126
127
|
matching_task_groups = [
|
|
@@ -132,7 +133,7 @@ async def _get_task_by_taskimport(
|
|
|
132
133
|
)
|
|
133
134
|
]
|
|
134
135
|
if len(matching_task_groups) < 1:
|
|
135
|
-
logger.
|
|
136
|
+
logger.debug(
|
|
136
137
|
"[_get_task_by_taskimport] "
|
|
137
138
|
f"No task group with {task_import.pkg_name=} "
|
|
138
139
|
f"and a task with {task_import.name=}."
|
|
@@ -142,13 +143,13 @@ async def _get_task_by_taskimport(
|
|
|
142
143
|
# Determine target `version`
|
|
143
144
|
# Note that task_import.version cannot be "", due to a validator
|
|
144
145
|
if task_import.version is None:
|
|
145
|
-
logger.
|
|
146
|
+
logger.debug(
|
|
146
147
|
"[_get_task_by_taskimport] "
|
|
147
148
|
"No version requested, looking for latest."
|
|
148
149
|
)
|
|
149
150
|
latest_task = max(matching_task_groups, key=lambda tg: tg.version or "")
|
|
150
151
|
version = latest_task.version
|
|
151
|
-
logger.
|
|
152
|
+
logger.debug(
|
|
152
153
|
f"[_get_task_by_taskimport] Latest version set to {version}."
|
|
153
154
|
)
|
|
154
155
|
else:
|
|
@@ -160,19 +161,19 @@ async def _get_task_by_taskimport(
|
|
|
160
161
|
)
|
|
161
162
|
|
|
162
163
|
if len(final_matching_task_groups) < 1:
|
|
163
|
-
logger.
|
|
164
|
+
logger.debug(
|
|
164
165
|
"[_get_task_by_taskimport] "
|
|
165
166
|
"No task group left after filtering by version."
|
|
166
167
|
)
|
|
167
168
|
return None
|
|
168
169
|
elif len(final_matching_task_groups) == 1:
|
|
169
170
|
final_task_group = final_matching_task_groups[0]
|
|
170
|
-
logger.
|
|
171
|
+
logger.debug(
|
|
171
172
|
"[_get_task_by_taskimport] "
|
|
172
173
|
"Found a single task group, after filtering by version."
|
|
173
174
|
)
|
|
174
175
|
else:
|
|
175
|
-
logger.
|
|
176
|
+
logger.debug(
|
|
176
177
|
"[_get_task_by_taskimport] "
|
|
177
178
|
f"Found {len(final_matching_task_groups)} task groups, "
|
|
178
179
|
"after filtering by version."
|
|
@@ -184,7 +185,7 @@ async def _get_task_by_taskimport(
|
|
|
184
185
|
default_group_id=default_group_id,
|
|
185
186
|
)
|
|
186
187
|
if final_task_group is None:
|
|
187
|
-
logger.
|
|
188
|
+
logger.debug(
|
|
188
189
|
"[_get_task_by_taskimport] Disambiguation returned None."
|
|
189
190
|
)
|
|
190
191
|
return None
|
|
@@ -199,7 +200,7 @@ async def _get_task_by_taskimport(
|
|
|
199
200
|
None,
|
|
200
201
|
)
|
|
201
202
|
|
|
202
|
-
logger.
|
|
203
|
+
logger.debug(f"[_get_task_by_taskimport] END, {task_import=}, {task_id=}.")
|
|
203
204
|
|
|
204
205
|
return task_id
|
|
205
206
|
|
|
@@ -222,9 +223,10 @@ async def import_workflow(
|
|
|
222
223
|
user_resource_id = await _get_user_resource_id(user_id=user.id, db=db)
|
|
223
224
|
|
|
224
225
|
# Preliminary checks
|
|
225
|
-
await
|
|
226
|
+
await _get_project_check_access(
|
|
226
227
|
project_id=project_id,
|
|
227
228
|
user_id=user.id,
|
|
229
|
+
required_permissions=ProjectPermissions.WRITE,
|
|
228
230
|
db=db,
|
|
229
231
|
)
|
|
230
232
|
await _check_workflow_exists(
|