fractal-server 2.17.1a1__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.
Files changed (225) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +21 -19
  3. fractal_server/app/db/__init__.py +3 -3
  4. fractal_server/app/models/__init__.py +1 -0
  5. fractal_server/app/models/linkuserproject.py +43 -1
  6. fractal_server/app/models/security.py +28 -8
  7. fractal_server/app/models/v2/__init__.py +3 -1
  8. fractal_server/app/models/v2/accounting.py +9 -1
  9. fractal_server/app/models/v2/dataset.py +5 -1
  10. fractal_server/app/models/v2/history.py +15 -1
  11. fractal_server/app/models/v2/job.py +17 -2
  12. fractal_server/app/models/v2/profile.py +29 -0
  13. fractal_server/app/models/v2/project.py +4 -10
  14. fractal_server/app/models/v2/resource.py +17 -0
  15. fractal_server/app/models/v2/task_group.py +4 -3
  16. fractal_server/app/models/v2/workflow.py +2 -1
  17. fractal_server/app/routes/admin/v2/__init__.py +12 -13
  18. fractal_server/app/routes/admin/v2/accounting.py +3 -3
  19. fractal_server/app/routes/admin/v2/job.py +35 -24
  20. fractal_server/app/routes/admin/v2/profile.py +3 -2
  21. fractal_server/app/routes/admin/v2/resource.py +5 -5
  22. fractal_server/app/routes/admin/v2/sharing.py +103 -0
  23. fractal_server/app/routes/admin/v2/task.py +37 -26
  24. fractal_server/app/routes/admin/v2/task_group.py +94 -17
  25. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +21 -22
  26. fractal_server/app/routes/api/__init__.py +1 -9
  27. fractal_server/app/routes/api/v2/__init__.py +49 -50
  28. fractal_server/app/routes/api/v2/_aux_functions.py +132 -124
  29. fractal_server/app/routes/api/v2/_aux_functions_history.py +51 -23
  30. fractal_server/app/routes/api/v2/_aux_functions_sharing.py +97 -0
  31. fractal_server/app/routes/api/v2/_aux_functions_task_lifecycle.py +6 -8
  32. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +7 -9
  33. fractal_server/app/routes/api/v2/_aux_task_group_disambiguation.py +1 -2
  34. fractal_server/app/routes/api/v2/dataset.py +95 -102
  35. fractal_server/app/routes/api/v2/history.py +59 -33
  36. fractal_server/app/routes/api/v2/images.py +24 -9
  37. fractal_server/app/routes/api/v2/job.py +52 -33
  38. fractal_server/app/routes/api/v2/pre_submission_checks.py +16 -8
  39. fractal_server/app/routes/api/v2/project.py +65 -37
  40. fractal_server/app/routes/api/v2/sharing.py +311 -0
  41. fractal_server/app/routes/api/v2/status_legacy.py +31 -41
  42. fractal_server/app/routes/api/v2/submit.py +82 -78
  43. fractal_server/app/routes/api/v2/task.py +19 -20
  44. fractal_server/app/routes/api/v2/task_collection.py +41 -43
  45. fractal_server/app/routes/api/v2/task_collection_custom.py +19 -20
  46. fractal_server/app/routes/api/v2/task_collection_pixi.py +10 -11
  47. fractal_server/app/routes/api/v2/task_group.py +25 -24
  48. fractal_server/app/routes/api/v2/task_group_lifecycle.py +32 -32
  49. fractal_server/app/routes/api/v2/task_version_update.py +23 -19
  50. fractal_server/app/routes/api/v2/workflow.py +50 -55
  51. fractal_server/app/routes/api/v2/workflow_import.py +37 -37
  52. fractal_server/app/routes/api/v2/workflowtask.py +32 -26
  53. fractal_server/app/routes/auth/__init__.py +1 -3
  54. fractal_server/app/routes/auth/_aux_auth.py +101 -2
  55. fractal_server/app/routes/auth/current_user.py +2 -66
  56. fractal_server/app/routes/auth/group.py +8 -35
  57. fractal_server/app/routes/auth/login.py +1 -0
  58. fractal_server/app/routes/auth/oauth.py +4 -3
  59. fractal_server/app/routes/auth/register.py +4 -2
  60. fractal_server/app/routes/auth/router.py +2 -0
  61. fractal_server/app/routes/auth/users.py +19 -10
  62. fractal_server/app/routes/auth/viewer_paths.py +43 -0
  63. fractal_server/app/routes/aux/_job.py +1 -1
  64. fractal_server/app/routes/aux/_runner.py +2 -2
  65. fractal_server/app/routes/pagination.py +1 -1
  66. fractal_server/app/schemas/user.py +29 -12
  67. fractal_server/app/schemas/user_group.py +0 -15
  68. fractal_server/app/schemas/v2/__init__.py +55 -48
  69. fractal_server/app/schemas/v2/accounting.py +11 -0
  70. fractal_server/app/schemas/v2/dataset.py +57 -11
  71. fractal_server/app/schemas/v2/dumps.py +10 -9
  72. fractal_server/app/schemas/v2/job.py +11 -11
  73. fractal_server/app/schemas/v2/manifest.py +4 -3
  74. fractal_server/app/schemas/v2/profile.py +53 -2
  75. fractal_server/app/schemas/v2/project.py +3 -3
  76. fractal_server/app/schemas/v2/resource.py +121 -16
  77. fractal_server/app/schemas/v2/sharing.py +99 -0
  78. fractal_server/app/schemas/v2/status_legacy.py +3 -3
  79. fractal_server/app/schemas/v2/task.py +6 -7
  80. fractal_server/app/schemas/v2/task_collection.py +5 -5
  81. fractal_server/app/schemas/v2/task_group.py +16 -16
  82. fractal_server/app/schemas/v2/workflow.py +16 -16
  83. fractal_server/app/schemas/v2/workflowtask.py +16 -15
  84. fractal_server/app/security/__init__.py +5 -8
  85. fractal_server/app/security/signup_email.py +4 -5
  86. fractal_server/app/shutdown.py +6 -6
  87. fractal_server/config/__init__.py +0 -6
  88. fractal_server/config/_data.py +0 -68
  89. fractal_server/config/_database.py +19 -20
  90. fractal_server/config/_email.py +30 -38
  91. fractal_server/config/_main.py +38 -52
  92. fractal_server/config/_oauth.py +17 -21
  93. fractal_server/data_migrations/2_18_0.py +30 -0
  94. fractal_server/exceptions.py +4 -0
  95. fractal_server/images/models.py +4 -5
  96. fractal_server/images/status_tools.py +4 -2
  97. fractal_server/logger.py +1 -1
  98. fractal_server/main.py +75 -13
  99. fractal_server/migrations/versions/034a469ec2eb_task_groups.py +4 -8
  100. fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py +1 -1
  101. fractal_server/migrations/versions/0f5f85bb2ae7_add_pre_pinned_packages.py +1 -0
  102. fractal_server/migrations/versions/19eca0dd47a9_user_settings_project_dir.py +1 -1
  103. fractal_server/migrations/versions/1a83a5260664_rename.py +1 -1
  104. fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +1 -0
  105. fractal_server/migrations/versions/316140ff7ee1_remove_usersettings_cache_dir.py +1 -1
  106. fractal_server/migrations/versions/40d6d6511b20_add_index_to_history_models.py +47 -0
  107. fractal_server/migrations/versions/45fbb391d7af_make_resource_id_fk_non_nullable.py +1 -1
  108. fractal_server/migrations/versions/47351f8c7ebc_drop_dataset_filters.py +1 -0
  109. fractal_server/migrations/versions/49d0856e9569_drop_table.py +2 -3
  110. fractal_server/migrations/versions/4c308bcaea2b_add_task_args_schema_and_task_args_.py +1 -1
  111. fractal_server/migrations/versions/4cedeb448a53_workflowtask_foreign_keys_not_nullables.py +1 -1
  112. fractal_server/migrations/versions/501961cfcd85_remove_link_between_v1_and_v2_tasks_.py +2 -1
  113. fractal_server/migrations/versions/50a13d6138fd_initial_schema.py +7 -19
  114. fractal_server/migrations/versions/5bf02391cfef_v2.py +4 -10
  115. fractal_server/migrations/versions/70e77f1c38b0_add_applyworkflow_first_task_index_and_.py +1 -0
  116. fractal_server/migrations/versions/71eefd1dd202_add_slurm_accounts.py +1 -1
  117. fractal_server/migrations/versions/7673fe18c05d_remove_project_dir_server_default.py +1 -1
  118. fractal_server/migrations/versions/7910eed4cf97_user_project_dirs_and_usergroup_viewer_.py +60 -0
  119. fractal_server/migrations/versions/791ce783d3d8_add_indices.py +1 -1
  120. fractal_server/migrations/versions/83bc2ad3ffcc_2_17_0.py +1 -0
  121. fractal_server/migrations/versions/84bf0fffde30_add_dumps_to_applyworkflow.py +1 -0
  122. fractal_server/migrations/versions/88270f589c9b_add_prevent_new_submissions.py +39 -0
  123. fractal_server/migrations/versions/8e8f227a3e36_update_taskv2_post_2_7_0.py +2 -4
  124. fractal_server/migrations/versions/8f79bd162e35_add_docs_info_and_docs_link_to_task_.py +1 -1
  125. fractal_server/migrations/versions/94a47ea2d3ff_remove_cache_dir_slurm_user_and_slurm_.py +1 -0
  126. fractal_server/migrations/versions/969d84257cac_add_historyrun_task_id.py +1 -1
  127. fractal_server/migrations/versions/97f444d47249_add_applyworkflow_project_dump.py +1 -1
  128. fractal_server/migrations/versions/981d588fe248_add_executor_error_log.py +1 -1
  129. fractal_server/migrations/versions/99ea79d9e5d2_add_dataset_history.py +2 -4
  130. fractal_server/migrations/versions/9c5ae74c9b98_add_user_settings_table.py +1 -1
  131. fractal_server/migrations/versions/9db60297b8b2_set_ondelete.py +1 -1
  132. fractal_server/migrations/versions/9fd26a2b0de4_add_workflow_timestamp_created.py +1 -1
  133. fractal_server/migrations/versions/a7f4d6137b53_add_workflow_dump_to_applyworkflow.py +1 -1
  134. fractal_server/migrations/versions/af1ef1c83c9b_add_accounting_tables.py +1 -0
  135. fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py +1 -0
  136. fractal_server/migrations/versions/b1e7f7a1ff71_task_group_for_pixi.py +1 -1
  137. fractal_server/migrations/versions/b3ffb095f973_json_to_jsonb.py +1 -0
  138. fractal_server/migrations/versions/bc0e8b3327a7_project_sharing.py +72 -0
  139. fractal_server/migrations/versions/c90a7c76e996_job_id_in_history_run.py +1 -1
  140. fractal_server/migrations/versions/caba9fb1ea5e_drop_useroauth_user_settings_id.py +1 -1
  141. fractal_server/migrations/versions/d256a7379ab8_taskgroup_activity_and_venv_info_to_.py +4 -9
  142. fractal_server/migrations/versions/d4fe3708d309_make_applyworkflow_workflow_dump_non_.py +1 -0
  143. fractal_server/migrations/versions/da2cb2ac4255_user_group_viewer_paths.py +1 -1
  144. fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +1 -0
  145. fractal_server/migrations/versions/e0e717ae2f26_delete_linkuserproject_ondelete_project.py +50 -0
  146. fractal_server/migrations/versions/e75cac726012_make_applyworkflow_start_timestamp_not_.py +1 -0
  147. fractal_server/migrations/versions/e81103413827_add_job_type_filters.py +1 -1
  148. fractal_server/migrations/versions/efa89c30e0a4_add_project_timestamp_created.py +1 -0
  149. fractal_server/migrations/versions/f0702066b007_one_submitted_job_per_dataset.py +40 -0
  150. fractal_server/migrations/versions/f37aceb45062_make_historyunit_logfile_required.py +1 -1
  151. fractal_server/migrations/versions/f384e1c0cf5d_drop_task_default_args_columns.py +1 -0
  152. fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +4 -9
  153. fractal_server/runner/config/_local.py +8 -5
  154. fractal_server/runner/config/_slurm.py +39 -33
  155. fractal_server/runner/config/slurm_mem_to_MB.py +0 -1
  156. fractal_server/runner/executors/base_runner.py +29 -4
  157. fractal_server/runner/executors/local/get_local_config.py +1 -0
  158. fractal_server/runner/executors/local/runner.py +14 -13
  159. fractal_server/runner/executors/slurm_common/_batching.py +9 -20
  160. fractal_server/runner/executors/slurm_common/base_slurm_runner.py +53 -27
  161. fractal_server/runner/executors/slurm_common/get_slurm_config.py +14 -7
  162. fractal_server/runner/executors/slurm_common/remote.py +3 -1
  163. fractal_server/runner/executors/slurm_common/slurm_config.py +2 -0
  164. fractal_server/runner/executors/slurm_common/slurm_job_task_models.py +1 -3
  165. fractal_server/runner/executors/slurm_ssh/runner.py +16 -11
  166. fractal_server/runner/executors/slurm_ssh/tar_commands.py +1 -0
  167. fractal_server/runner/executors/slurm_sudo/_subprocess_run_as_user.py +1 -0
  168. fractal_server/runner/executors/slurm_sudo/runner.py +16 -11
  169. fractal_server/runner/task_files.py +9 -3
  170. fractal_server/runner/v2/_local.py +12 -6
  171. fractal_server/runner/v2/_slurm_ssh.py +14 -7
  172. fractal_server/runner/v2/_slurm_sudo.py +14 -7
  173. fractal_server/runner/v2/db_tools.py +0 -1
  174. fractal_server/runner/v2/deduplicate_list.py +2 -1
  175. fractal_server/runner/v2/runner.py +44 -28
  176. fractal_server/runner/v2/runner_functions.py +22 -28
  177. fractal_server/runner/v2/submit_workflow.py +29 -15
  178. fractal_server/ssh/_fabric.py +6 -13
  179. fractal_server/string_tools.py +0 -1
  180. fractal_server/syringe.py +1 -1
  181. fractal_server/tasks/config/_pixi.py +1 -1
  182. fractal_server/tasks/config/_python.py +16 -9
  183. fractal_server/tasks/utils.py +0 -1
  184. fractal_server/tasks/v2/local/_utils.py +3 -3
  185. fractal_server/tasks/v2/local/collect.py +15 -18
  186. fractal_server/tasks/v2/local/collect_pixi.py +14 -16
  187. fractal_server/tasks/v2/local/deactivate.py +14 -15
  188. fractal_server/tasks/v2/local/deactivate_pixi.py +7 -7
  189. fractal_server/tasks/v2/local/delete.py +6 -8
  190. fractal_server/tasks/v2/local/reactivate.py +12 -12
  191. fractal_server/tasks/v2/local/reactivate_pixi.py +12 -12
  192. fractal_server/tasks/v2/ssh/_utils.py +3 -3
  193. fractal_server/tasks/v2/ssh/collect.py +19 -24
  194. fractal_server/tasks/v2/ssh/collect_pixi.py +22 -24
  195. fractal_server/tasks/v2/ssh/deactivate.py +17 -15
  196. fractal_server/tasks/v2/ssh/deactivate_pixi.py +8 -7
  197. fractal_server/tasks/v2/ssh/delete.py +12 -10
  198. fractal_server/tasks/v2/ssh/reactivate.py +16 -16
  199. fractal_server/tasks/v2/ssh/reactivate_pixi.py +13 -14
  200. fractal_server/tasks/v2/templates/1_create_venv.sh +2 -0
  201. fractal_server/tasks/v2/templates/2_pip_install.sh +2 -0
  202. fractal_server/tasks/v2/templates/3_pip_freeze.sh +2 -0
  203. fractal_server/tasks/v2/templates/4_pip_show.sh +2 -0
  204. fractal_server/tasks/v2/templates/5_get_venv_size_and_file_number.sh +3 -1
  205. fractal_server/tasks/v2/templates/6_pip_install_from_freeze.sh +2 -0
  206. fractal_server/tasks/v2/templates/pixi_1_extract.sh +2 -0
  207. fractal_server/tasks/v2/templates/pixi_2_install.sh +2 -0
  208. fractal_server/tasks/v2/templates/pixi_3_post_install.sh +2 -0
  209. fractal_server/tasks/v2/utils_background.py +10 -10
  210. fractal_server/tasks/v2/utils_database.py +5 -5
  211. fractal_server/tasks/v2/utils_package_names.py +1 -2
  212. fractal_server/tasks/v2/utils_pixi.py +1 -3
  213. fractal_server/types/__init__.py +98 -1
  214. fractal_server/types/validators/__init__.py +3 -0
  215. fractal_server/types/validators/_common_validators.py +33 -3
  216. fractal_server/types/validators/_workflow_task_arguments_validators.py +1 -2
  217. fractal_server/utils.py +1 -0
  218. fractal_server/zip_tools.py +34 -0
  219. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/METADATA +3 -2
  220. fractal_server-2.18.0.dist-info/RECORD +275 -0
  221. fractal_server/app/routes/admin/v2/project.py +0 -41
  222. fractal_server-2.17.1a1.dist-info/RECORD +0 -264
  223. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/WHEEL +0 -0
  224. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/entry_points.txt +0 -0
  225. {fractal_server-2.17.1a1.dist-info → fractal_server-2.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,19 +1,27 @@
1
+ from os.path import normpath
2
+ from pathlib import Path
3
+
1
4
  from fastapi import HTTPException
2
5
  from fastapi import status
3
6
  from sqlalchemy.ext.asyncio import AsyncSession
7
+ from sqlmodel import and_
4
8
  from sqlmodel import asc
9
+ from sqlmodel import not_
10
+ from sqlmodel import or_
5
11
  from sqlmodel import select
6
12
 
7
13
  from fractal_server.app.models.linkusergroup import LinkUserGroup
14
+ from fractal_server.app.models.linkuserproject import LinkUserProjectV2
8
15
  from fractal_server.app.models.security import UserGroup
9
16
  from fractal_server.app.models.security import UserOAuth
17
+ from fractal_server.app.models.v2.dataset import DatasetV2
18
+ from fractal_server.app.models.v2.project import ProjectV2
10
19
  from fractal_server.app.schemas.user import UserRead
11
20
  from fractal_server.app.schemas.user_group import UserGroupRead
12
21
  from fractal_server.config import get_settings
13
22
  from fractal_server.logger import set_logger
14
23
  from fractal_server.syringe import Inject
15
24
 
16
-
17
25
  logger = set_logger(__name__)
18
26
 
19
27
 
@@ -36,7 +44,7 @@ async def _get_single_user_with_groups(
36
44
 
37
45
  stm_groups = (
38
46
  select(UserGroup)
39
- .join(LinkUserGroup)
47
+ .join(LinkUserGroup, LinkUserGroup.group_id == UserGroup.id)
40
48
  .where(LinkUserGroup.user_id == user.id)
41
49
  .order_by(asc(LinkUserGroup.timestamp_created))
42
50
  )
@@ -179,3 +187,94 @@ async def _verify_user_belongs_to_group(
179
187
  f"to UserGroup {user_group_id}"
180
188
  ),
181
189
  )
190
+
191
+
192
+ async def _check_project_dirs_update(
193
+ *,
194
+ old_project_dirs: list[str],
195
+ new_project_dirs: list[str],
196
+ user_id: int,
197
+ db: AsyncSession,
198
+ ) -> None:
199
+ """
200
+ Raises 422 if by replacing user's `project_dirs` with new ones we are
201
+ removing the access to a `zarr_dir` used by some dataset.
202
+
203
+ Note both `old_project_dirs` and `new_project_dirs` have been
204
+ normalized through `os.path.normpath`, which notably strips any trailing
205
+ `/` character. To be safe, we also re-normalize them within this function.
206
+ """
207
+ # Create a list of all the old project dirs that will lose privileges.
208
+ # E.g.:
209
+ # old_project_dirs = ["/a", "/b", "/c/d", "/e/f"]
210
+ # new_project_dirs = ["/a", "/c", "/e/f/g1", "/e/f/g2"]
211
+ # removed_project_dirs == ["/b", "/e/f"]
212
+ removed_project_dirs = [
213
+ old_project_dir
214
+ for old_project_dir in old_project_dirs
215
+ if not any(
216
+ Path(old_project_dir).is_relative_to(new_project_dir)
217
+ for new_project_dir in new_project_dirs
218
+ )
219
+ ]
220
+ if removed_project_dirs:
221
+ # Query all the `zarr_dir`s linked to the user such that `zarr_dir`
222
+ # starts with one of the project dirs in `removed_project_dirs`.
223
+ stmt = (
224
+ select(DatasetV2.zarr_dir)
225
+ .join(ProjectV2, ProjectV2.id == DatasetV2.project_id)
226
+ .join(
227
+ LinkUserProjectV2,
228
+ LinkUserProjectV2.project_id == ProjectV2.id,
229
+ )
230
+ .where(LinkUserProjectV2.user_id == user_id)
231
+ .where(LinkUserProjectV2.is_verified.is_(True))
232
+ .where(
233
+ or_(
234
+ *[
235
+ DatasetV2.zarr_dir.startswith(normpath(old_project_dir))
236
+ for old_project_dir in removed_project_dirs
237
+ ]
238
+ )
239
+ )
240
+ )
241
+ if new_project_dirs:
242
+ stmt = stmt.where(
243
+ and_(
244
+ *[
245
+ not_(
246
+ DatasetV2.zarr_dir.startswith(
247
+ normpath(new_project_dir)
248
+ )
249
+ )
250
+ for new_project_dir in new_project_dirs
251
+ ]
252
+ )
253
+ )
254
+ res = await db.execute(stmt)
255
+
256
+ # Raise 422 if one of the query results is relative to a path in
257
+ # `removed_project_dirs`, but its not relative to any path in
258
+ # `new_project_dirs`.
259
+ if any(
260
+ (
261
+ any(
262
+ Path(zarr_dir).is_relative_to(old_project_dir)
263
+ for old_project_dir in removed_project_dirs
264
+ )
265
+ and not any(
266
+ Path(zarr_dir).is_relative_to(new_project_dir)
267
+ for new_project_dir in new_project_dirs
268
+ )
269
+ )
270
+ for zarr_dir in res.scalars().all()
271
+ ):
272
+ raise HTTPException(
273
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
274
+ detail=(
275
+ "You tried updating the user project_dirs, removing "
276
+ f"{removed_project_dirs}. This operation is not possible, "
277
+ "because it would make the user loose access to some of "
278
+ "their dataset zarr directories."
279
+ ),
280
+ )
@@ -1,7 +1,6 @@
1
1
  """
2
2
  Definition of `/auth/current-user/` endpoints
3
3
  """
4
- import os
5
4
 
6
5
  from fastapi import APIRouter
7
6
  from fastapi import Depends
@@ -9,13 +8,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
9
8
  from sqlmodel import select
10
9
 
11
10
  from fractal_server.app.db import get_async_db
12
- from fractal_server.app.models import LinkUserGroup
13
11
  from fractal_server.app.models import Profile
14
12
  from fractal_server.app.models import Resource
15
- from fractal_server.app.models import UserGroup
16
13
  from fractal_server.app.models import UserOAuth
17
14
  from fractal_server.app.routes.auth import current_user_act
18
- from fractal_server.app.routes.auth import current_user_act_ver
19
15
  from fractal_server.app.routes.auth._aux_auth import (
20
16
  _get_single_user_with_groups,
21
17
  )
@@ -23,11 +19,8 @@ from fractal_server.app.schemas import UserProfileInfo
23
19
  from fractal_server.app.schemas.user import UserRead
24
20
  from fractal_server.app.schemas.user import UserUpdate
25
21
  from fractal_server.app.schemas.user import UserUpdateStrict
26
- from fractal_server.app.security import get_user_manager
27
22
  from fractal_server.app.security import UserManager
28
- from fractal_server.config import DataAuthScheme
29
- from fractal_server.config import get_data_settings
30
- from fractal_server.syringe import Inject
23
+ from fractal_server.app.security import get_user_manager
31
24
 
32
25
  router_current_user = APIRouter()
33
26
 
@@ -87,9 +80,8 @@ async def get_current_user_profile_info(
87
80
  ) -> UserProfileInfo:
88
81
  stm = (
89
82
  select(Resource, Profile)
90
- .join(UserOAuth)
83
+ .join(UserOAuth, Profile.id == UserOAuth.profile_id)
91
84
  .where(Resource.id == Profile.resource_id)
92
- .where(Profile.id == UserOAuth.profile_id)
93
85
  .where(UserOAuth.id == current_user.id)
94
86
  )
95
87
  res = await db.execute(stm)
@@ -106,59 +98,3 @@ async def get_current_user_profile_info(
106
98
  )
107
99
 
108
100
  return response_data
109
-
110
-
111
- @router_current_user.get(
112
- "/current-user/allowed-viewer-paths/", response_model=list[str]
113
- )
114
- async def get_current_user_allowed_viewer_paths(
115
- current_user: UserOAuth = Depends(current_user_act_ver),
116
- db: AsyncSession = Depends(get_async_db),
117
- ) -> list[str]:
118
- """
119
- Returns the allowed viewer paths for current user, according to the
120
- selected FRACTAL_DATA_AUTH_SCHEME
121
- """
122
-
123
- data_settings = Inject(get_data_settings)
124
-
125
- authorized_paths = []
126
-
127
- if data_settings.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.NONE:
128
- return authorized_paths
129
-
130
- # Append `project_dir` to the list of authorized paths
131
- authorized_paths.append(current_user.project_dir)
132
-
133
- # If auth scheme is "users-folders" and `slurm_user` is set,
134
- # build and append the user folder
135
- if (
136
- data_settings.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.USERS_FOLDERS
137
- and current_user.profile_id is not None
138
- ):
139
- profile = await db.get(Profile, current_user.profile_id)
140
- if profile is not None and profile.username is not None:
141
- base_folder = data_settings.FRACTAL_DATA_BASE_FOLDER
142
- user_folder = os.path.join(base_folder, profile.username)
143
- authorized_paths.append(user_folder)
144
-
145
- if data_settings.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.VIEWER_PATHS:
146
- # Returns the union of `viewer_paths` for all user's groups
147
- cmd = (
148
- select(UserGroup.viewer_paths)
149
- .join(LinkUserGroup)
150
- .where(LinkUserGroup.group_id == UserGroup.id)
151
- .where(LinkUserGroup.user_id == current_user.id)
152
- )
153
- res = await db.execute(cmd)
154
- viewer_paths_nested = res.scalars().all()
155
-
156
- # Flatten a nested object and make its elements unique
157
- all_viewer_paths_set = {
158
- path
159
- for _viewer_paths in viewer_paths_nested
160
- for path in _viewer_paths
161
- }
162
- authorized_paths.extend(all_viewer_paths_set)
163
-
164
- return authorized_paths
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Definition of `/auth/group/` routes
3
3
  """
4
+
4
5
  from fastapi import APIRouter
5
6
  from fastapi import Depends
6
7
  from fastapi import HTTPException
@@ -9,22 +10,22 @@ from fastapi import status
9
10
  from sqlalchemy.ext.asyncio import AsyncSession
10
11
  from sqlmodel import select
11
12
 
12
- from . import current_superuser_act
13
- from ._aux_auth import _get_default_usergroup_id_or_none
14
- from ._aux_auth import _get_single_usergroup_with_user_ids
15
- from ._aux_auth import _user_or_404
16
- from ._aux_auth import _usergroup_or_404
17
13
  from fractal_server.app.db import get_async_db
18
14
  from fractal_server.app.models import LinkUserGroup
19
15
  from fractal_server.app.models import UserGroup
20
16
  from fractal_server.app.models import UserOAuth
21
17
  from fractal_server.app.schemas.user_group import UserGroupCreate
22
18
  from fractal_server.app.schemas.user_group import UserGroupRead
23
- from fractal_server.app.schemas.user_group import UserGroupUpdate
24
19
  from fractal_server.config import get_settings
25
20
  from fractal_server.logger import set_logger
26
21
  from fractal_server.syringe import Inject
27
22
 
23
+ from . import current_superuser_act
24
+ from ._aux_auth import _get_default_usergroup_id_or_none
25
+ from ._aux_auth import _get_single_usergroup_with_user_ids
26
+ from ._aux_auth import _user_or_404
27
+ from ._aux_auth import _usergroup_or_404
28
+
28
29
  logger = set_logger(__name__)
29
30
 
30
31
 
@@ -99,41 +100,13 @@ async def create_single_group(
99
100
  )
100
101
 
101
102
  # Create and return new group
102
- new_group = UserGroup(
103
- name=group_create.name, viewer_paths=group_create.viewer_paths
104
- )
103
+ new_group = UserGroup(name=group_create.name)
105
104
  db.add(new_group)
106
105
  await db.commit()
107
106
 
108
107
  return dict(new_group.model_dump(), user_ids=[])
109
108
 
110
109
 
111
- @router_group.patch(
112
- "/group/{group_id}/",
113
- response_model=UserGroupRead,
114
- status_code=status.HTTP_200_OK,
115
- )
116
- async def update_single_group(
117
- group_id: int,
118
- group_update: UserGroupUpdate,
119
- user: UserOAuth = Depends(current_superuser_act),
120
- db: AsyncSession = Depends(get_async_db),
121
- ) -> UserGroupRead:
122
- group = await _usergroup_or_404(group_id, db)
123
-
124
- # Patch `viewer_paths`
125
- if group_update.viewer_paths is not None:
126
- group.viewer_paths = group_update.viewer_paths
127
- db.add(group)
128
- await db.commit()
129
-
130
- updated_group = await _get_single_usergroup_with_user_ids(
131
- group_id=group_id, db=db
132
- )
133
-
134
- return updated_group
135
-
136
-
137
110
  @router_group.delete("/group/{group_id}/", status_code=204)
138
111
  async def delete_single_group(
139
112
  group_id: int,
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Definition of `/auth/{login,logout}/`, `/auth/token/{login/logout}` routes.
3
3
  """
4
+
4
5
  from fastapi import APIRouter
5
6
 
6
7
  from . import cookie_backend
@@ -4,13 +4,14 @@ from httpx_oauth.clients.google import GoogleOAuth2
4
4
  from httpx_oauth.clients.openid import OpenID
5
5
  from httpx_oauth.clients.openid import OpenIDConfigurationError
6
6
 
7
- from . import cookie_backend
8
- from . import fastapi_users
7
+ from fractal_server.config import OAuthSettings
9
8
  from fractal_server.config import get_oauth_settings
10
9
  from fractal_server.config import get_settings
11
- from fractal_server.config import OAuthSettings
12
10
  from fractal_server.syringe import Inject
13
11
 
12
+ from . import cookie_backend
13
+ from . import fastapi_users
14
+
14
15
 
15
16
  def _create_client_github(cfg: OAuthSettings) -> GitHubOAuth2:
16
17
  return GitHubOAuth2(
@@ -1,13 +1,15 @@
1
1
  """
2
2
  Definition of `/auth/register/` routes.
3
3
  """
4
+
4
5
  from fastapi import APIRouter
5
6
  from fastapi import Depends
6
7
 
8
+ from fractal_server.app.schemas.user import UserCreate
9
+ from fractal_server.app.schemas.user import UserRead
10
+
7
11
  from . import current_superuser_act
8
12
  from . import fastapi_users
9
- from ...schemas.user import UserCreate
10
- from ...schemas.user import UserRead
11
13
 
12
14
  router_register = APIRouter()
13
15
 
@@ -6,6 +6,7 @@ from .login import router_login
6
6
  from .oauth import get_oauth_router
7
7
  from .register import router_register
8
8
  from .users import router_users
9
+ from .viewer_paths import router_viewer_paths
9
10
 
10
11
  router_auth = APIRouter()
11
12
 
@@ -14,6 +15,7 @@ router_auth.include_router(router_current_user)
14
15
  router_auth.include_router(router_login)
15
16
  router_auth.include_router(router_users)
16
17
  router_auth.include_router(router_group)
18
+ router_auth.include_router(router_viewer_paths)
17
19
  router_oauth = get_oauth_router()
18
20
  if router_oauth is not None:
19
21
  router_auth.include_router(router_oauth)
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Definition of `/auth/users/` routes
3
3
  """
4
+
4
5
  from fastapi import APIRouter
5
6
  from fastapi import Depends
6
7
  from fastapi import HTTPException
@@ -11,24 +12,25 @@ from sqlalchemy.ext.asyncio import AsyncSession
11
12
  from sqlmodel import func
12
13
  from sqlmodel import select
13
14
 
14
- from . import current_superuser_act
15
- from ...db import get_async_db
16
- from ...schemas.user import UserRead
17
- from ...schemas.user import UserUpdate
18
- from ._aux_auth import _get_default_usergroup_id_or_none
19
- from ._aux_auth import _get_single_user_with_groups
15
+ from fractal_server.app.db import get_async_db
20
16
  from fractal_server.app.models import LinkUserGroup
21
17
  from fractal_server.app.models import UserGroup
22
18
  from fractal_server.app.models import UserOAuth
23
19
  from fractal_server.app.models.v2 import Profile
24
20
  from fractal_server.app.routes.auth._aux_auth import _user_or_404
21
+ from fractal_server.app.schemas.user import UserRead
22
+ from fractal_server.app.schemas.user import UserUpdate
25
23
  from fractal_server.app.schemas.user import UserUpdateGroups
26
- from fractal_server.app.security import get_user_manager
27
24
  from fractal_server.app.security import UserManager
25
+ from fractal_server.app.security import get_user_manager
28
26
  from fractal_server.config import get_settings
29
27
  from fractal_server.logger import set_logger
30
28
  from fractal_server.syringe import Inject
31
29
 
30
+ from . import current_superuser_act
31
+ from ._aux_auth import _check_project_dirs_update
32
+ from ._aux_auth import _get_default_usergroup_id_or_none
33
+ from ._aux_auth import _get_single_user_with_groups
32
34
 
33
35
  router_users = APIRouter()
34
36
 
@@ -73,6 +75,14 @@ async def patch_user(
73
75
  detail=f"Profile {user_update.profile_id} not found.",
74
76
  )
75
77
 
78
+ if user_update.project_dirs is not None:
79
+ await _check_project_dirs_update(
80
+ old_project_dirs=user_to_patch.project_dirs,
81
+ new_project_dirs=user_update.project_dirs,
82
+ user_id=user_id,
83
+ db=db,
84
+ )
85
+
76
86
  # Modify user attributes
77
87
  try:
78
88
  user = await user_manager.update(
@@ -197,13 +207,12 @@ async def set_user_groups(
197
207
  # Remove/create links as needed
198
208
  for link in links_to_remove:
199
209
  logger.info(
200
- f"Removing LinkUserGroup with {link.user_id=} "
201
- f"and {link.group_id=}."
210
+ f"Removing LinkUserGroup with {link.user_id=} and {link.group_id=}."
202
211
  )
203
212
  await db.delete(link)
204
213
  for group_id in ids_links_to_add:
205
214
  logger.info(
206
- f"Creating new LinkUserGroup with {user_id=} " f"and {group_id=}."
215
+ f"Creating new LinkUserGroup with {user_id=} and {group_id=}."
207
216
  )
208
217
  db.add(LinkUserGroup(user_id=user_id, group_id=group_id))
209
218
  await db.commit()
@@ -0,0 +1,43 @@
1
+ from fastapi import APIRouter
2
+ from fastapi import Depends
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlmodel import select
5
+
6
+ from fractal_server.app.db import get_async_db
7
+ from fractal_server.app.models import UserOAuth
8
+ from fractal_server.app.models.linkuserproject import LinkUserProjectV2
9
+ from fractal_server.app.models.v2.dataset import DatasetV2
10
+ from fractal_server.app.models.v2.project import ProjectV2
11
+ from fractal_server.app.routes.auth import current_user_act_ver
12
+
13
+ router_viewer_paths = APIRouter()
14
+
15
+
16
+ @router_viewer_paths.get(
17
+ "/current-user/allowed-viewer-paths/", response_model=list[str]
18
+ )
19
+ async def get_current_user_allowed_viewer_paths(
20
+ include_shared_projects: bool = True,
21
+ current_user: UserOAuth = Depends(current_user_act_ver),
22
+ db: AsyncSession = Depends(get_async_db),
23
+ ) -> list[str]:
24
+ """
25
+ Returns the allowed viewer paths for current user.
26
+ """
27
+ authorized_paths = current_user.project_dirs.copy()
28
+
29
+ if include_shared_projects:
30
+ res = await db.execute(
31
+ select(DatasetV2.zarr_dir)
32
+ .join(ProjectV2, ProjectV2.id == DatasetV2.project_id)
33
+ .join(
34
+ LinkUserProjectV2, LinkUserProjectV2.project_id == ProjectV2.id
35
+ )
36
+ .where(LinkUserProjectV2.user_id == current_user.id)
37
+ .where(LinkUserProjectV2.is_verified.is_(True))
38
+ )
39
+ authorized_paths.extend(res.unique().scalars().all())
40
+ # Note that `project_dirs` and the `db.execute` result may have some
41
+ # common elements, and then this list may have non-unique items.
42
+
43
+ return authorized_paths
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from ...models.v2 import JobV2
3
+ from fractal_server.app.models.v2 import JobV2
4
4
  from fractal_server.runner.filenames import SHUTDOWN_FILENAME
5
5
 
6
6
 
@@ -1,9 +1,9 @@
1
1
  from fastapi import HTTPException
2
2
  from fastapi import status
3
3
 
4
- from ....config import get_settings
5
- from ....syringe import Inject
6
4
  from fractal_server.app.schemas.v2 import ResourceType
5
+ from fractal_server.config import get_settings
6
+ from fractal_server.syringe import Inject
7
7
 
8
8
 
9
9
  def _backend_supports_shutdown(backend: str) -> bool:
@@ -4,8 +4,8 @@ from typing import TypeVar
4
4
  from fastapi import HTTPException
5
5
  from pydantic import BaseModel
6
6
  from pydantic import Field
7
- from pydantic import model_validator
8
7
  from pydantic import ValidationError
8
+ from pydantic import model_validator
9
9
 
10
10
  T = TypeVar("T")
11
11
 
@@ -8,12 +8,18 @@ from pydantic import EmailStr
8
8
  from pydantic import Field
9
9
 
10
10
  from fractal_server.string_tools import validate_cmd
11
- from fractal_server.types import AbsolutePathStr
11
+ from fractal_server.types import ListUniqueAbsolutePathStr
12
12
  from fractal_server.types import ListUniqueNonEmptyString
13
13
  from fractal_server.types import ListUniqueNonNegativeInt
14
14
  from fractal_server.types import NonEmptyStr
15
15
 
16
16
 
17
+ def _validate_cmd_list(value: list[str]) -> list[str]:
18
+ for v in value:
19
+ validate_cmd(v)
20
+ return value
21
+
22
+
17
23
  class OAuthAccountRead(BaseModel):
18
24
  """
19
25
  Schema for storing essential `OAuthAccount` information within
@@ -38,20 +44,17 @@ class UserRead(schemas.BaseUser[int]):
38
44
  group_ids_names:
39
45
  oauth_accounts:
40
46
  profile_id:
47
+ project_dirs:
48
+ slurm_accounts:
41
49
  """
42
50
 
43
51
  group_ids_names: list[tuple[int, str]] | None = None
44
52
  oauth_accounts: list[OAuthAccountRead]
45
53
  profile_id: int | None = None
46
- project_dir: str
54
+ project_dirs: list[str]
47
55
  slurm_accounts: list[str]
48
56
 
49
57
 
50
- def _validate_cmd(value: str) -> str:
51
- validate_cmd(value)
52
- return value
53
-
54
-
55
58
  class UserUpdate(schemas.BaseUserUpdate):
56
59
  """
57
60
  Schema for `User` update.
@@ -63,7 +66,7 @@ class UserUpdate(schemas.BaseUserUpdate):
63
66
  is_superuser:
64
67
  is_verified:
65
68
  profile_id:
66
- project_dir:
69
+ project_dirs:
67
70
  slurm_accounts:
68
71
  """
69
72
 
@@ -74,9 +77,9 @@ class UserUpdate(schemas.BaseUserUpdate):
74
77
  is_superuser: bool = None
75
78
  is_verified: bool = None
76
79
  profile_id: int | None = None
77
- project_dir: Annotated[
78
- AbsolutePathStr, AfterValidator(_validate_cmd)
79
- ] = None
80
+ project_dirs: Annotated[
81
+ ListUniqueAbsolutePathStr, AfterValidator(_validate_cmd_list)
82
+ ] = Field(default=None, min_length=1)
80
83
  slurm_accounts: ListUniqueNonEmptyString = None
81
84
 
82
85
 
@@ -98,10 +101,14 @@ class UserCreate(schemas.BaseUserCreate):
98
101
 
99
102
  Attributes:
100
103
  profile_id:
104
+ project_dirs:
105
+ slurm_accounts:
101
106
  """
102
107
 
103
108
  profile_id: int | None = None
104
- project_dir: Annotated[AbsolutePathStr, AfterValidator(_validate_cmd)]
109
+ project_dirs: Annotated[
110
+ ListUniqueAbsolutePathStr, AfterValidator(_validate_cmd_list)
111
+ ] = Field(min_length=1)
105
112
  slurm_accounts: list[str] = Field(default_factory=list)
106
113
 
107
114
 
@@ -109,6 +116,8 @@ class UserUpdateGroups(BaseModel):
109
116
  """
110
117
  Schema for `POST /auth/users/{user_id}/set-groups/`
111
118
 
119
+ Attributes:
120
+ group_ids:
112
121
  """
113
122
 
114
123
  model_config = ConfigDict(extra="forbid")
@@ -117,6 +126,14 @@ class UserUpdateGroups(BaseModel):
117
126
 
118
127
 
119
128
  class UserProfileInfo(BaseModel):
129
+ """
130
+ Attributes:
131
+ has_profile:
132
+ resource_name:
133
+ profile_name:
134
+ username:
135
+ """
136
+
120
137
  has_profile: bool
121
138
  resource_name: str | None = None
122
139
  profile_name: str | None = None
@@ -2,16 +2,13 @@ from datetime import datetime
2
2
 
3
3
  from pydantic import BaseModel
4
4
  from pydantic import ConfigDict
5
- from pydantic import Field
6
5
  from pydantic import field_serializer
7
6
  from pydantic.types import AwareDatetime
8
7
 
9
- from fractal_server.types import ListUniqueAbsolutePathStr
10
8
  from fractal_server.types import NonEmptyStr
11
9
 
12
10
  __all__ = (
13
11
  "UserGroupRead",
14
- "UserGroupUpdate",
15
12
  "UserGroupCreate",
16
13
  )
17
14
 
@@ -34,7 +31,6 @@ class UserGroupRead(BaseModel):
34
31
  name: str
35
32
  timestamp_created: AwareDatetime
36
33
  user_ids: list[int] | None = None
37
- viewer_paths: list[str]
38
34
 
39
35
  @field_serializer("timestamp_created")
40
36
  def serialize_datetime(v: datetime) -> str:
@@ -52,14 +48,3 @@ class UserGroupCreate(BaseModel):
52
48
  model_config = ConfigDict(extra="forbid")
53
49
 
54
50
  name: NonEmptyStr
55
- viewer_paths: ListUniqueAbsolutePathStr = Field(default_factory=list)
56
-
57
-
58
- class UserGroupUpdate(BaseModel):
59
- """
60
- Schema for `UserGroup` update
61
- """
62
-
63
- model_config = ConfigDict(extra="forbid")
64
-
65
- viewer_paths: ListUniqueAbsolutePathStr = None