fractal-server 2.8.1__py3-none-any.whl → 2.9.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/app/db/__init__.py +2 -35
- fractal_server/app/models/v2/__init__.py +3 -3
- fractal_server/app/models/v2/task.py +0 -72
- fractal_server/app/models/v2/task_group.py +113 -0
- fractal_server/app/routes/admin/v1.py +13 -30
- fractal_server/app/routes/admin/v2/__init__.py +4 -0
- fractal_server/app/routes/admin/v2/job.py +13 -24
- fractal_server/app/routes/admin/v2/task.py +13 -0
- fractal_server/app/routes/admin/v2/task_group.py +75 -14
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +267 -0
- fractal_server/app/routes/api/v1/project.py +7 -19
- fractal_server/app/routes/api/v2/__init__.py +11 -2
- fractal_server/app/routes/api/v2/{_aux_functions_task_collection.py → _aux_functions_task_lifecycle.py} +83 -0
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +27 -17
- fractal_server/app/routes/api/v2/submit.py +19 -24
- fractal_server/app/routes/api/v2/task_collection.py +33 -65
- fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
- fractal_server/app/routes/api/v2/task_group.py +86 -14
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +272 -0
- fractal_server/app/routes/api/v2/workflow.py +1 -1
- fractal_server/app/routes/api/v2/workflow_import.py +2 -2
- fractal_server/app/routes/auth/current_user.py +60 -17
- fractal_server/app/routes/auth/group.py +67 -39
- fractal_server/app/routes/auth/users.py +97 -99
- fractal_server/app/routes/aux/__init__.py +20 -0
- fractal_server/app/runner/executors/slurm/_slurm_config.py +0 -17
- fractal_server/app/runner/executors/slurm/ssh/executor.py +49 -204
- fractal_server/app/runner/executors/slurm/sudo/executor.py +26 -109
- fractal_server/app/runner/executors/slurm/utils_executors.py +58 -0
- fractal_server/app/runner/v2/_local_experimental/executor.py +2 -1
- fractal_server/app/schemas/_validators.py +0 -15
- fractal_server/app/schemas/user.py +16 -10
- fractal_server/app/schemas/user_group.py +0 -11
- fractal_server/app/schemas/v1/applyworkflow.py +0 -8
- fractal_server/app/schemas/v1/dataset.py +0 -5
- fractal_server/app/schemas/v1/project.py +0 -5
- fractal_server/app/schemas/v1/state.py +0 -5
- fractal_server/app/schemas/v1/workflow.py +0 -5
- fractal_server/app/schemas/v2/__init__.py +4 -2
- fractal_server/app/schemas/v2/dataset.py +0 -6
- fractal_server/app/schemas/v2/job.py +0 -8
- fractal_server/app/schemas/v2/project.py +0 -5
- fractal_server/app/schemas/v2/task_collection.py +0 -21
- fractal_server/app/schemas/v2/task_group.py +59 -8
- fractal_server/app/schemas/v2/workflow.py +0 -5
- fractal_server/app/security/__init__.py +17 -0
- fractal_server/config.py +61 -59
- fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +117 -0
- fractal_server/ssh/_fabric.py +156 -83
- fractal_server/tasks/utils.py +2 -12
- fractal_server/tasks/v2/local/__init__.py +3 -0
- fractal_server/tasks/v2/local/_utils.py +70 -0
- fractal_server/tasks/v2/local/collect.py +291 -0
- fractal_server/tasks/v2/local/deactivate.py +218 -0
- fractal_server/tasks/v2/local/reactivate.py +159 -0
- fractal_server/tasks/v2/ssh/__init__.py +3 -0
- fractal_server/tasks/v2/ssh/_utils.py +87 -0
- fractal_server/tasks/v2/ssh/collect.py +311 -0
- fractal_server/tasks/v2/ssh/deactivate.py +253 -0
- fractal_server/tasks/v2/ssh/reactivate.py +202 -0
- fractal_server/tasks/v2/templates/{_2_preliminary_pip_operations.sh → 1_create_venv.sh} +6 -7
- fractal_server/tasks/v2/templates/{_3_pip_install.sh → 2_pip_install.sh} +8 -1
- fractal_server/tasks/v2/templates/{_4_pip_freeze.sh → 3_pip_freeze.sh} +0 -7
- fractal_server/tasks/v2/templates/{_5_pip_show.sh → 4_pip_show.sh} +5 -6
- fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +10 -0
- fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +35 -0
- fractal_server/tasks/v2/utils_background.py +42 -127
- fractal_server/tasks/v2/utils_templates.py +32 -2
- fractal_server/utils.py +4 -2
- fractal_server/zip_tools.py +21 -4
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/METADATA +3 -5
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/RECORD +77 -64
- fractal_server/app/models/v2/collection_state.py +0 -22
- fractal_server/tasks/v2/collection_local.py +0 -357
- fractal_server/tasks/v2/collection_ssh.py +0 -352
- fractal_server/tasks/v2/templates/_1_create_venv.sh +0 -42
- /fractal_server/tasks/v2/{database_operations.py → utils_database.py} +0 -0
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.8.1.dist-info → fractal_server-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,272 @@
|
|
1
|
+
from fastapi import APIRouter
|
2
|
+
from fastapi import BackgroundTasks
|
3
|
+
from fastapi import Depends
|
4
|
+
from fastapi import HTTPException
|
5
|
+
from fastapi import Request
|
6
|
+
from fastapi import Response
|
7
|
+
from fastapi import status
|
8
|
+
|
9
|
+
from ...aux.validate_user_settings import validate_user_settings
|
10
|
+
from ._aux_functions_task_lifecycle import check_no_ongoing_activity
|
11
|
+
from ._aux_functions_task_lifecycle import check_no_submitted_job
|
12
|
+
from ._aux_functions_tasks import _get_task_group_full_access
|
13
|
+
from fractal_server.app.db import AsyncSession
|
14
|
+
from fractal_server.app.db import get_async_db
|
15
|
+
from fractal_server.app.models import UserOAuth
|
16
|
+
from fractal_server.app.models.v2 import TaskGroupActivityV2
|
17
|
+
from fractal_server.app.routes.auth import current_active_user
|
18
|
+
from fractal_server.app.schemas.v2 import TaskGroupActivityActionV2
|
19
|
+
from fractal_server.app.schemas.v2 import TaskGroupActivityStatusV2
|
20
|
+
from fractal_server.app.schemas.v2 import TaskGroupActivityV2Read
|
21
|
+
from fractal_server.app.schemas.v2 import TaskGroupReadV2
|
22
|
+
from fractal_server.app.schemas.v2 import TaskGroupV2OriginEnum
|
23
|
+
from fractal_server.config import get_settings
|
24
|
+
from fractal_server.logger import set_logger
|
25
|
+
from fractal_server.syringe import Inject
|
26
|
+
from fractal_server.tasks.v2.local import deactivate_local
|
27
|
+
from fractal_server.tasks.v2.local import reactivate_local
|
28
|
+
from fractal_server.tasks.v2.ssh import deactivate_ssh
|
29
|
+
from fractal_server.tasks.v2.ssh import reactivate_ssh
|
30
|
+
from fractal_server.utils import get_timestamp
|
31
|
+
|
32
|
+
router = APIRouter()
|
33
|
+
|
34
|
+
|
35
|
+
logger = set_logger(__name__)
|
36
|
+
|
37
|
+
|
38
|
+
@router.post(
|
39
|
+
"/{task_group_id}/deactivate/",
|
40
|
+
response_model=TaskGroupActivityV2Read,
|
41
|
+
)
|
42
|
+
async def deactivate_task_group(
|
43
|
+
task_group_id: int,
|
44
|
+
background_tasks: BackgroundTasks,
|
45
|
+
response: Response,
|
46
|
+
request: Request,
|
47
|
+
user: UserOAuth = Depends(current_active_user),
|
48
|
+
db: AsyncSession = Depends(get_async_db),
|
49
|
+
) -> TaskGroupReadV2:
|
50
|
+
"""
|
51
|
+
Deactivate task-group venv
|
52
|
+
"""
|
53
|
+
# Check access
|
54
|
+
task_group = await _get_task_group_full_access(
|
55
|
+
task_group_id=task_group_id,
|
56
|
+
user_id=user.id,
|
57
|
+
db=db,
|
58
|
+
)
|
59
|
+
|
60
|
+
# Check no other activity is ongoing
|
61
|
+
await check_no_ongoing_activity(task_group_id=task_group_id, db=db)
|
62
|
+
|
63
|
+
# Check no submitted jobs use tasks from this task group
|
64
|
+
await check_no_submitted_job(task_group_id=task_group.id, db=db)
|
65
|
+
|
66
|
+
# Check that task-group is active
|
67
|
+
if not task_group.active:
|
68
|
+
raise HTTPException(
|
69
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
70
|
+
detail=(
|
71
|
+
f"Cannot deactivate a task group with {task_group.active=}."
|
72
|
+
),
|
73
|
+
)
|
74
|
+
|
75
|
+
# Shortcut for task-group with origin="other"
|
76
|
+
if task_group.origin == TaskGroupV2OriginEnum.OTHER:
|
77
|
+
task_group.active = False
|
78
|
+
task_group_activity = TaskGroupActivityV2(
|
79
|
+
user_id=task_group.user_id,
|
80
|
+
taskgroupv2_id=task_group.id,
|
81
|
+
status=TaskGroupActivityStatusV2.OK,
|
82
|
+
action=TaskGroupActivityActionV2.DEACTIVATE,
|
83
|
+
pkg_name=task_group.pkg_name,
|
84
|
+
version=(task_group.version or "N/A"),
|
85
|
+
log=(
|
86
|
+
f"Task group has {task_group.origin=}, set "
|
87
|
+
"task_group.active to False and exit."
|
88
|
+
),
|
89
|
+
timestamp_started=get_timestamp(),
|
90
|
+
timestamp_ended=get_timestamp(),
|
91
|
+
)
|
92
|
+
db.add(task_group)
|
93
|
+
db.add(task_group_activity)
|
94
|
+
await db.commit()
|
95
|
+
response.status_code = status.HTTP_202_ACCEPTED
|
96
|
+
return task_group_activity
|
97
|
+
|
98
|
+
task_group_activity = TaskGroupActivityV2(
|
99
|
+
user_id=task_group.user_id,
|
100
|
+
taskgroupv2_id=task_group.id,
|
101
|
+
status=TaskGroupActivityStatusV2.PENDING,
|
102
|
+
action=TaskGroupActivityActionV2.DEACTIVATE,
|
103
|
+
pkg_name=task_group.pkg_name,
|
104
|
+
version=task_group.version,
|
105
|
+
timestamp_started=get_timestamp(),
|
106
|
+
)
|
107
|
+
task_group.active = False
|
108
|
+
db.add(task_group)
|
109
|
+
db.add(task_group_activity)
|
110
|
+
await db.commit()
|
111
|
+
|
112
|
+
# Submit background task
|
113
|
+
settings = Inject(get_settings)
|
114
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
115
|
+
|
116
|
+
# Validate user settings (backend-specific)
|
117
|
+
user_settings = await validate_user_settings(
|
118
|
+
user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
|
119
|
+
)
|
120
|
+
|
121
|
+
# User appropriate FractalSSH object
|
122
|
+
ssh_credentials = dict(
|
123
|
+
user=user_settings.ssh_username,
|
124
|
+
host=user_settings.ssh_host,
|
125
|
+
key_path=user_settings.ssh_private_key_path,
|
126
|
+
)
|
127
|
+
fractal_ssh_list = request.app.state.fractal_ssh_list
|
128
|
+
fractal_ssh = fractal_ssh_list.get(**ssh_credentials)
|
129
|
+
|
130
|
+
background_tasks.add_task(
|
131
|
+
deactivate_ssh,
|
132
|
+
task_group_id=task_group.id,
|
133
|
+
task_group_activity_id=task_group_activity.id,
|
134
|
+
fractal_ssh=fractal_ssh,
|
135
|
+
tasks_base_dir=user_settings.ssh_tasks_dir,
|
136
|
+
)
|
137
|
+
|
138
|
+
else:
|
139
|
+
background_tasks.add_task(
|
140
|
+
deactivate_local,
|
141
|
+
task_group_id=task_group.id,
|
142
|
+
task_group_activity_id=task_group_activity.id,
|
143
|
+
)
|
144
|
+
|
145
|
+
logger.debug(
|
146
|
+
"Task group deactivation endpoint: start deactivate "
|
147
|
+
"and return task_group_activity"
|
148
|
+
)
|
149
|
+
response.status_code = status.HTTP_202_ACCEPTED
|
150
|
+
return task_group_activity
|
151
|
+
|
152
|
+
|
153
|
+
@router.post(
|
154
|
+
"/{task_group_id}/reactivate/",
|
155
|
+
response_model=TaskGroupActivityV2Read,
|
156
|
+
)
|
157
|
+
async def reactivate_task_group(
|
158
|
+
task_group_id: int,
|
159
|
+
background_tasks: BackgroundTasks,
|
160
|
+
response: Response,
|
161
|
+
request: Request,
|
162
|
+
user: UserOAuth = Depends(current_active_user),
|
163
|
+
db: AsyncSession = Depends(get_async_db),
|
164
|
+
) -> TaskGroupReadV2:
|
165
|
+
"""
|
166
|
+
Deactivate task-group venv
|
167
|
+
"""
|
168
|
+
|
169
|
+
# Check access
|
170
|
+
task_group = await _get_task_group_full_access(
|
171
|
+
task_group_id=task_group_id,
|
172
|
+
user_id=user.id,
|
173
|
+
db=db,
|
174
|
+
)
|
175
|
+
|
176
|
+
# Check that task-group is not active
|
177
|
+
if task_group.active:
|
178
|
+
raise HTTPException(
|
179
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
180
|
+
detail=(
|
181
|
+
f"Cannot reactivate a task group with {task_group.active=}."
|
182
|
+
),
|
183
|
+
)
|
184
|
+
|
185
|
+
# Check no other activity is ongoing
|
186
|
+
await check_no_ongoing_activity(task_group_id=task_group_id, db=db)
|
187
|
+
|
188
|
+
# Check no submitted jobs use tasks from this task group
|
189
|
+
await check_no_submitted_job(task_group_id=task_group.id, db=db)
|
190
|
+
|
191
|
+
# Shortcut for task-group with origin="other"
|
192
|
+
if task_group.origin == TaskGroupV2OriginEnum.OTHER:
|
193
|
+
task_group.active = True
|
194
|
+
task_group_activity = TaskGroupActivityV2(
|
195
|
+
user_id=task_group.user_id,
|
196
|
+
taskgroupv2_id=task_group.id,
|
197
|
+
status=TaskGroupActivityStatusV2.OK,
|
198
|
+
action=TaskGroupActivityActionV2.REACTIVATE,
|
199
|
+
pkg_name=task_group.pkg_name,
|
200
|
+
version=(task_group.version or "N/A"),
|
201
|
+
log=(
|
202
|
+
f"Task group has {task_group.origin=}, set "
|
203
|
+
"task_group.active to True and exit."
|
204
|
+
),
|
205
|
+
timestamp_started=get_timestamp(),
|
206
|
+
timestamp_ended=get_timestamp(),
|
207
|
+
)
|
208
|
+
db.add(task_group)
|
209
|
+
db.add(task_group_activity)
|
210
|
+
await db.commit()
|
211
|
+
response.status_code = status.HTTP_202_ACCEPTED
|
212
|
+
return task_group_activity
|
213
|
+
|
214
|
+
if task_group.pip_freeze is None:
|
215
|
+
raise HTTPException(
|
216
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
217
|
+
detail=(
|
218
|
+
"Cannot reactivate a task group with "
|
219
|
+
f"{task_group.pip_freeze=}."
|
220
|
+
),
|
221
|
+
)
|
222
|
+
|
223
|
+
task_group_activity = TaskGroupActivityV2(
|
224
|
+
user_id=task_group.user_id,
|
225
|
+
taskgroupv2_id=task_group.id,
|
226
|
+
status=TaskGroupActivityStatusV2.PENDING,
|
227
|
+
action=TaskGroupActivityActionV2.REACTIVATE,
|
228
|
+
pkg_name=task_group.pkg_name,
|
229
|
+
version=task_group.version,
|
230
|
+
timestamp_started=get_timestamp(),
|
231
|
+
)
|
232
|
+
db.add(task_group_activity)
|
233
|
+
await db.commit()
|
234
|
+
|
235
|
+
# Submit background task
|
236
|
+
settings = Inject(get_settings)
|
237
|
+
if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
|
238
|
+
|
239
|
+
# Validate user settings (backend-specific)
|
240
|
+
user_settings = await validate_user_settings(
|
241
|
+
user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
|
242
|
+
)
|
243
|
+
|
244
|
+
# Use appropriate FractalSSH object
|
245
|
+
ssh_credentials = dict(
|
246
|
+
user=user_settings.ssh_username,
|
247
|
+
host=user_settings.ssh_host,
|
248
|
+
key_path=user_settings.ssh_private_key_path,
|
249
|
+
)
|
250
|
+
fractal_ssh_list = request.app.state.fractal_ssh_list
|
251
|
+
fractal_ssh = fractal_ssh_list.get(**ssh_credentials)
|
252
|
+
|
253
|
+
background_tasks.add_task(
|
254
|
+
reactivate_ssh,
|
255
|
+
task_group_id=task_group.id,
|
256
|
+
task_group_activity_id=task_group_activity.id,
|
257
|
+
fractal_ssh=fractal_ssh,
|
258
|
+
tasks_base_dir=user_settings.ssh_tasks_dir,
|
259
|
+
)
|
260
|
+
|
261
|
+
else:
|
262
|
+
background_tasks.add_task(
|
263
|
+
reactivate_local,
|
264
|
+
task_group_id=task_group.id,
|
265
|
+
task_group_activity_id=task_group_activity.id,
|
266
|
+
)
|
267
|
+
logger.debug(
|
268
|
+
"Task group reactivation endpoint: start reactivate "
|
269
|
+
"and return task_group_activity"
|
270
|
+
)
|
271
|
+
response.status_code = status.HTTP_202_ACCEPTED
|
272
|
+
return task_group_activity
|
@@ -23,7 +23,7 @@ from ._aux_functions import _get_submitted_jobs_statement
|
|
23
23
|
from ._aux_functions import _get_workflow_check_owner
|
24
24
|
from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
|
25
25
|
from fractal_server.app.models import UserOAuth
|
26
|
-
from fractal_server.app.models.v2
|
26
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
27
27
|
from fractal_server.app.routes.auth import current_active_user
|
28
28
|
|
29
29
|
router = APIRouter()
|
@@ -21,10 +21,10 @@ from ._aux_functions import _workflow_insert_task
|
|
21
21
|
from ._aux_functions_tasks import _add_warnings_to_workflow_tasks
|
22
22
|
from fractal_server.app.models import LinkUserGroup
|
23
23
|
from fractal_server.app.models import UserOAuth
|
24
|
-
from fractal_server.app.models.v2
|
24
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
25
25
|
from fractal_server.app.routes.auth import current_active_user
|
26
26
|
from fractal_server.app.routes.auth._aux_auth import _get_default_usergroup_id
|
27
|
-
from fractal_server.app.schemas.v2
|
27
|
+
from fractal_server.app.schemas.v2 import TaskImportV2
|
28
28
|
from fractal_server.logger import set_logger
|
29
29
|
|
30
30
|
router = APIRouter()
|
@@ -1,6 +1,8 @@
|
|
1
1
|
"""
|
2
2
|
Definition of `/auth/current-user/` endpoints
|
3
3
|
"""
|
4
|
+
import os
|
5
|
+
|
4
6
|
from fastapi import APIRouter
|
5
7
|
from fastapi import Depends
|
6
8
|
from fastapi_users import schemas
|
@@ -22,6 +24,8 @@ from fractal_server.app.schemas import UserSettingsReadStrict
|
|
22
24
|
from fractal_server.app.schemas import UserSettingsUpdateStrict
|
23
25
|
from fractal_server.app.security import get_user_manager
|
24
26
|
from fractal_server.app.security import UserManager
|
27
|
+
from fractal_server.config import get_settings
|
28
|
+
from fractal_server.syringe import Inject
|
25
29
|
|
26
30
|
router_current_user = APIRouter()
|
27
31
|
|
@@ -109,26 +113,65 @@ async def patch_current_user_settings(
|
|
109
113
|
|
110
114
|
|
111
115
|
@router_current_user.get(
|
112
|
-
"/current-user/viewer-paths/", response_model=list[str]
|
116
|
+
"/current-user/allowed-viewer-paths/", response_model=list[str]
|
113
117
|
)
|
114
|
-
async def
|
118
|
+
async def get_current_user_allowed_viewer_paths(
|
115
119
|
current_user: UserOAuth = Depends(current_active_user),
|
116
120
|
db: AsyncSession = Depends(get_async_db),
|
117
121
|
) -> list[str]:
|
118
|
-
"""
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
.where(LinkUserGroup.group_id == UserGroup.id)
|
123
|
-
.where(LinkUserGroup.user_id == current_user.id)
|
124
|
-
)
|
125
|
-
res = await db.execute(cmd)
|
126
|
-
viewer_paths_nested = res.scalars().all()
|
122
|
+
"""
|
123
|
+
Returns the allowed viewer paths for current user, according to the
|
124
|
+
selected FRACTAL_VIEWER_AUTHORIZATION_SCHEME
|
125
|
+
"""
|
127
126
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
all_viewer_paths = list(all_viewer_paths_set)
|
127
|
+
settings = Inject(get_settings)
|
128
|
+
|
129
|
+
if settings.FRACTAL_VIEWER_AUTHORIZATION_SCHEME == "none":
|
130
|
+
return []
|
133
131
|
|
134
|
-
|
132
|
+
authorized_paths = []
|
133
|
+
|
134
|
+
# Respond with 422 error if user has no settings
|
135
|
+
verify_user_has_settings(current_user)
|
136
|
+
|
137
|
+
# Load current user settings
|
138
|
+
current_user_settings = await db.get(
|
139
|
+
UserSettings, current_user.user_settings_id
|
140
|
+
)
|
141
|
+
# If project_dir is set, append it to the list of authorized paths
|
142
|
+
if current_user_settings.project_dir is not None:
|
143
|
+
authorized_paths.append(current_user_settings.project_dir)
|
144
|
+
|
145
|
+
# If auth scheme is "users-folders" and `slurm_user` is set,
|
146
|
+
# build and append the user folder
|
147
|
+
if (
|
148
|
+
settings.FRACTAL_VIEWER_AUTHORIZATION_SCHEME == "users-folders"
|
149
|
+
and current_user_settings.slurm_user is not None
|
150
|
+
):
|
151
|
+
base_folder = settings.FRACTAL_VIEWER_BASE_FOLDER
|
152
|
+
user_folder = os.path.join(
|
153
|
+
base_folder, current_user_settings.slurm_user
|
154
|
+
)
|
155
|
+
authorized_paths.append(user_folder)
|
156
|
+
|
157
|
+
if settings.FRACTAL_VIEWER_AUTHORIZATION_SCHEME == "viewer-paths":
|
158
|
+
# Returns the union of `viewer_paths` for all user's groups
|
159
|
+
cmd = (
|
160
|
+
select(UserGroup.viewer_paths)
|
161
|
+
.join(LinkUserGroup)
|
162
|
+
.where(LinkUserGroup.group_id == UserGroup.id)
|
163
|
+
.where(LinkUserGroup.user_id == current_user.id)
|
164
|
+
)
|
165
|
+
res = await db.execute(cmd)
|
166
|
+
viewer_paths_nested = res.scalars().all()
|
167
|
+
|
168
|
+
# Flatten a nested object and make its elements unique
|
169
|
+
all_viewer_paths_set = set(
|
170
|
+
path
|
171
|
+
for _viewer_paths in viewer_paths_nested
|
172
|
+
for path in _viewer_paths
|
173
|
+
)
|
174
|
+
|
175
|
+
authorized_paths.extend(all_viewer_paths_set)
|
176
|
+
|
177
|
+
return authorized_paths
|
@@ -6,14 +6,13 @@ from fastapi import Depends
|
|
6
6
|
from fastapi import HTTPException
|
7
7
|
from fastapi import Response
|
8
8
|
from fastapi import status
|
9
|
-
from sqlalchemy.exc import IntegrityError
|
10
9
|
from sqlalchemy.ext.asyncio import AsyncSession
|
11
|
-
from sqlmodel import col
|
12
|
-
from sqlmodel import func
|
13
10
|
from sqlmodel import select
|
14
11
|
|
15
12
|
from . import current_active_superuser
|
13
|
+
from ._aux_auth import _get_default_usergroup_id
|
16
14
|
from ._aux_auth import _get_single_usergroup_with_user_ids
|
15
|
+
from ._aux_auth import _user_or_404
|
17
16
|
from ._aux_auth import _usergroup_or_404
|
18
17
|
from fractal_server.app.db import get_async_db
|
19
18
|
from fractal_server.app.models import LinkUserGroup
|
@@ -126,42 +125,6 @@ async def update_single_group(
|
|
126
125
|
|
127
126
|
group = await _usergroup_or_404(group_id, db)
|
128
127
|
|
129
|
-
# Check that all required users exist
|
130
|
-
# Note: The reason for introducing `col` is as in
|
131
|
-
# https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
|
132
|
-
stm = select(func.count()).where(
|
133
|
-
col(UserOAuth.id).in_(group_update.new_user_ids)
|
134
|
-
)
|
135
|
-
res = await db.execute(stm)
|
136
|
-
number_matching_users = res.scalar()
|
137
|
-
if number_matching_users != len(group_update.new_user_ids):
|
138
|
-
raise HTTPException(
|
139
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
140
|
-
detail=(
|
141
|
-
f"Not all requested users (IDs {group_update.new_user_ids}) "
|
142
|
-
"exist."
|
143
|
-
),
|
144
|
-
)
|
145
|
-
|
146
|
-
# Add new users to existing group
|
147
|
-
for user_id in group_update.new_user_ids:
|
148
|
-
link = LinkUserGroup(user_id=user_id, group_id=group_id)
|
149
|
-
db.add(link)
|
150
|
-
try:
|
151
|
-
await db.commit()
|
152
|
-
except IntegrityError as e:
|
153
|
-
error_msg = (
|
154
|
-
f"Cannot link users with IDs {group_update.new_user_ids} "
|
155
|
-
f"to group {group_id}. "
|
156
|
-
"Likely reason: one of these links already exists.\n"
|
157
|
-
f"Original error: {str(e)}"
|
158
|
-
)
|
159
|
-
logger.info(error_msg)
|
160
|
-
raise HTTPException(
|
161
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
162
|
-
detail=error_msg,
|
163
|
-
)
|
164
|
-
|
165
128
|
# Patch `viewer_paths`
|
166
129
|
if group_update.viewer_paths is not None:
|
167
130
|
group.viewer_paths = group_update.viewer_paths
|
@@ -239,3 +202,68 @@ async def patch_user_settings_bulk(
|
|
239
202
|
await db.commit()
|
240
203
|
|
241
204
|
return Response(status_code=status.HTTP_200_OK)
|
205
|
+
|
206
|
+
|
207
|
+
@router_group.post("/group/{group_id}/add-user/{user_id}/", status_code=200)
|
208
|
+
async def add_user_to_group(
|
209
|
+
group_id: int,
|
210
|
+
user_id: int,
|
211
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
212
|
+
db: AsyncSession = Depends(get_async_db),
|
213
|
+
) -> UserGroupRead:
|
214
|
+
await _usergroup_or_404(group_id, db)
|
215
|
+
user = await _user_or_404(user_id, db)
|
216
|
+
link = await db.get(LinkUserGroup, (group_id, user_id))
|
217
|
+
if link is None:
|
218
|
+
db.add(LinkUserGroup(group_id=group_id, user_id=user_id))
|
219
|
+
await db.commit()
|
220
|
+
else:
|
221
|
+
raise HTTPException(
|
222
|
+
status_code=422,
|
223
|
+
detail=(
|
224
|
+
f"User '{user.email}' is already a member of group {group_id}."
|
225
|
+
),
|
226
|
+
)
|
227
|
+
group = await _get_single_usergroup_with_user_ids(group_id=group_id, db=db)
|
228
|
+
return group
|
229
|
+
|
230
|
+
|
231
|
+
@router_group.post("/group/{group_id}/remove-user/{user_id}/", status_code=200)
|
232
|
+
async def remove_user_from_group(
|
233
|
+
group_id: int,
|
234
|
+
user_id: int,
|
235
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
236
|
+
db: AsyncSession = Depends(get_async_db),
|
237
|
+
) -> UserGroupRead:
|
238
|
+
|
239
|
+
# Check that user and group exist
|
240
|
+
await _usergroup_or_404(group_id, db)
|
241
|
+
user = await _user_or_404(user_id, db)
|
242
|
+
|
243
|
+
# Check that group is not the default one
|
244
|
+
default_user_group_id = await _get_default_usergroup_id(db=db)
|
245
|
+
if default_user_group_id == group_id:
|
246
|
+
raise HTTPException(
|
247
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
248
|
+
detail=(
|
249
|
+
f"Cannot remove user from '{FRACTAL_DEFAULT_GROUP_NAME}' "
|
250
|
+
"group.",
|
251
|
+
),
|
252
|
+
)
|
253
|
+
|
254
|
+
link = await db.get(LinkUserGroup, (group_id, user_id))
|
255
|
+
if link is None:
|
256
|
+
# If user and group are not linked, fail
|
257
|
+
raise HTTPException(
|
258
|
+
status_code=422,
|
259
|
+
detail=f"User '{user.email}' is not a member of group {group_id}.",
|
260
|
+
)
|
261
|
+
else:
|
262
|
+
# If user and group are linked, delete the link
|
263
|
+
await db.delete(link)
|
264
|
+
await db.commit()
|
265
|
+
|
266
|
+
# Enrich the response object with user_ids
|
267
|
+
group = await _get_single_usergroup_with_user_ids(group_id=group_id, db=db)
|
268
|
+
|
269
|
+
return group
|