fractal-server 2.3.11__py3-none-any.whl → 2.4.0a1__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 (53) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +25 -2
  3. fractal_server/app/models/__init__.py +11 -5
  4. fractal_server/app/models/linkusergroup.py +11 -0
  5. fractal_server/app/models/security.py +24 -3
  6. fractal_server/app/models/v1/project.py +1 -1
  7. fractal_server/app/models/v2/project.py +3 -3
  8. fractal_server/app/routes/admin/v1.py +14 -14
  9. fractal_server/app/routes/admin/v2.py +12 -12
  10. fractal_server/app/routes/api/__init__.py +2 -2
  11. fractal_server/app/routes/api/v1/_aux_functions.py +2 -2
  12. fractal_server/app/routes/api/v1/dataset.py +17 -15
  13. fractal_server/app/routes/api/v1/job.py +11 -9
  14. fractal_server/app/routes/api/v1/project.py +9 -9
  15. fractal_server/app/routes/api/v1/task.py +8 -8
  16. fractal_server/app/routes/api/v1/task_collection.py +5 -5
  17. fractal_server/app/routes/api/v1/workflow.py +13 -11
  18. fractal_server/app/routes/api/v1/workflowtask.py +6 -6
  19. fractal_server/app/routes/api/v2/_aux_functions.py +2 -2
  20. fractal_server/app/routes/api/v2/dataset.py +11 -11
  21. fractal_server/app/routes/api/v2/images.py +6 -6
  22. fractal_server/app/routes/api/v2/job.py +9 -9
  23. fractal_server/app/routes/api/v2/project.py +7 -7
  24. fractal_server/app/routes/api/v2/status.py +3 -3
  25. fractal_server/app/routes/api/v2/submit.py +3 -3
  26. fractal_server/app/routes/api/v2/task.py +8 -8
  27. fractal_server/app/routes/api/v2/task_collection.py +5 -5
  28. fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
  29. fractal_server/app/routes/api/v2/task_legacy.py +9 -9
  30. fractal_server/app/routes/api/v2/workflow.py +11 -11
  31. fractal_server/app/routes/api/v2/workflowtask.py +6 -6
  32. fractal_server/app/routes/auth/__init__.py +55 -0
  33. fractal_server/app/routes/auth/_aux_auth.py +107 -0
  34. fractal_server/app/routes/auth/current_user.py +60 -0
  35. fractal_server/app/routes/auth/group.py +159 -0
  36. fractal_server/app/routes/auth/group_names.py +34 -0
  37. fractal_server/app/routes/auth/login.py +25 -0
  38. fractal_server/app/routes/auth/oauth.py +63 -0
  39. fractal_server/app/routes/auth/register.py +23 -0
  40. fractal_server/app/routes/auth/router.py +19 -0
  41. fractal_server/app/routes/auth/users.py +173 -0
  42. fractal_server/app/schemas/user.py +7 -0
  43. fractal_server/app/schemas/user_group.py +57 -0
  44. fractal_server/app/security/__init__.py +72 -75
  45. fractal_server/data_migrations/2_4_0.py +61 -0
  46. fractal_server/main.py +1 -9
  47. fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py +53 -0
  48. {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/METADATA +1 -1
  49. {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/RECORD +52 -39
  50. fractal_server/app/routes/auth.py +0 -165
  51. {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/LICENSE +0 -0
  52. {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/WHEEL +0 -0
  53. {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/entry_points.txt +0 -0
@@ -18,10 +18,10 @@ from ....models.v2 import WorkflowV2
18
18
  from ....schemas.v2 import TaskCreateV2
19
19
  from ....schemas.v2 import TaskReadV2
20
20
  from ....schemas.v2 import TaskUpdateV2
21
- from ....security import current_active_user
22
- from ....security import current_active_verified_user
23
- from ....security import User
24
21
  from ._aux_functions import _get_task_check_owner
22
+ from fractal_server.app.models import UserOAuth
23
+ from fractal_server.app.routes.auth import current_active_user
24
+ from fractal_server.app.routes.auth import current_active_verified_user
25
25
 
26
26
  router = APIRouter()
27
27
 
@@ -32,7 +32,7 @@ logger = set_logger(__name__)
32
32
  async def get_list_task(
33
33
  args_schema_parallel: bool = True,
34
34
  args_schema_non_parallel: bool = True,
35
- user: User = Depends(current_active_user),
35
+ user: UserOAuth = Depends(current_active_user),
36
36
  db: AsyncSession = Depends(get_async_db),
37
37
  ) -> list[TaskReadV2]:
38
38
  """
@@ -55,7 +55,7 @@ async def get_list_task(
55
55
  @router.get("/{task_id}/", response_model=TaskReadV2)
56
56
  async def get_task(
57
57
  task_id: int,
58
- user: User = Depends(current_active_user),
58
+ user: UserOAuth = Depends(current_active_user),
59
59
  db: AsyncSession = Depends(get_async_db),
60
60
  ) -> TaskReadV2:
61
61
  """
@@ -74,7 +74,7 @@ async def get_task(
74
74
  async def patch_task(
75
75
  task_id: int,
76
76
  task_update: TaskUpdateV2,
77
- user: User = Depends(current_active_verified_user),
77
+ user: UserOAuth = Depends(current_active_verified_user),
78
78
  db: AsyncSession = Depends(get_async_db),
79
79
  ) -> Optional[TaskReadV2]:
80
80
  """
@@ -111,7 +111,7 @@ async def patch_task(
111
111
  )
112
112
  async def create_task(
113
113
  task: TaskCreateV2,
114
- user: User = Depends(current_active_verified_user),
114
+ user: UserOAuth = Depends(current_active_verified_user),
115
115
  db: AsyncSession = Depends(get_async_db),
116
116
  ) -> Optional[TaskReadV2]:
117
117
  """
@@ -193,7 +193,7 @@ async def create_task(
193
193
  @router.delete("/{task_id}/", status_code=204)
194
194
  async def delete_task(
195
195
  task_id: int,
196
- user: User = Depends(current_active_user),
196
+ user: UserOAuth = Depends(current_active_user),
197
197
  db: AsyncSession = Depends(get_async_db),
198
198
  ) -> Response:
199
199
  """
@@ -25,9 +25,9 @@ from ....schemas.v2 import CollectionStateReadV2
25
25
  from ....schemas.v2 import CollectionStatusV2
26
26
  from ....schemas.v2 import TaskCollectPipV2
27
27
  from ....schemas.v2 import TaskReadV2
28
- from ....security import current_active_user
29
- from ....security import current_active_verified_user
30
- from ....security import User
28
+ from fractal_server.app.models import UserOAuth
29
+ from fractal_server.app.routes.auth import current_active_user
30
+ from fractal_server.app.routes.auth import current_active_verified_user
31
31
  from fractal_server.string_tools import slugify_task_name_for_source
32
32
  from fractal_server.tasks.utils import get_absolute_venv_path
33
33
  from fractal_server.tasks.utils import get_collection_log
@@ -69,7 +69,7 @@ async def collect_tasks_pip(
69
69
  background_tasks: BackgroundTasks,
70
70
  response: Response,
71
71
  request: Request,
72
- user: User = Depends(current_active_verified_user),
72
+ user: UserOAuth = Depends(current_active_verified_user),
73
73
  db: AsyncSession = Depends(get_async_db),
74
74
  ) -> CollectionStateReadV2:
75
75
  """
@@ -289,7 +289,7 @@ async def collect_tasks_pip(
289
289
  @router.get("/collect/{state_id}/", response_model=CollectionStateReadV2)
290
290
  async def check_collection_status(
291
291
  state_id: int,
292
- user: User = Depends(current_active_user),
292
+ user: UserOAuth = Depends(current_active_user),
293
293
  verbose: bool = False,
294
294
  db: AsyncSession = Depends(get_async_db),
295
295
  ) -> CollectionStateReadV2: # State[TaskCollectStatus]
@@ -18,8 +18,8 @@ from ....models.v2 import TaskV2
18
18
  from ....schemas.v2 import TaskCollectCustomV2
19
19
  from ....schemas.v2 import TaskCreateV2
20
20
  from ....schemas.v2 import TaskReadV2
21
- from ....security import current_active_verified_user
22
- from ....security import User
21
+ from fractal_server.app.models import UserOAuth
22
+ from fractal_server.app.routes.auth import current_active_verified_user
23
23
  from fractal_server.tasks.v2.background_operations import _insert_tasks
24
24
  from fractal_server.tasks.v2.background_operations import (
25
25
  _prepare_tasks_metadata,
@@ -36,7 +36,7 @@ logger = set_logger(__name__)
36
36
  )
37
37
  async def collect_task_custom(
38
38
  task_collect: TaskCollectCustomV2,
39
- user: User = Depends(current_active_verified_user),
39
+ user: UserOAuth = Depends(current_active_verified_user),
40
40
  db: DBSyncSession = Depends(get_sync_db),
41
41
  ) -> list[TaskReadV2]:
42
42
 
@@ -4,13 +4,13 @@ from fastapi import HTTPException
4
4
  from fastapi import status
5
5
  from sqlmodel import select
6
6
 
7
- from .....logger import set_logger
8
- from ....db import AsyncSession
9
- from ....db import get_async_db
10
- from ....models.v1 import Task as TaskV1
11
- from ....schemas.v2 import TaskLegacyReadV2
12
- from ....security import current_active_user
13
- from ....security import User
7
+ from fractal_server.app.db import AsyncSession
8
+ from fractal_server.app.db import get_async_db
9
+ from fractal_server.app.models import UserOAuth
10
+ from fractal_server.app.models.v1 import Task as TaskV1
11
+ from fractal_server.app.routes.auth import current_active_user
12
+ from fractal_server.app.schemas.v2 import TaskLegacyReadV2
13
+ from fractal_server.logger import set_logger
14
14
 
15
15
  router = APIRouter()
16
16
 
@@ -21,7 +21,7 @@ logger = set_logger(__name__)
21
21
  async def get_list_task_legacy(
22
22
  args_schema: bool = True,
23
23
  only_v2_compatible: bool = False,
24
- user: User = Depends(current_active_user),
24
+ user: UserOAuth = Depends(current_active_user),
25
25
  db: AsyncSession = Depends(get_async_db),
26
26
  ) -> list[TaskLegacyReadV2]:
27
27
  """
@@ -43,7 +43,7 @@ async def get_list_task_legacy(
43
43
  @router.get("/{task_id}/", response_model=TaskLegacyReadV2)
44
44
  async def get_task_legacy(
45
45
  task_id: int,
46
- user: User = Depends(current_active_user),
46
+ user: UserOAuth = Depends(current_active_user),
47
47
  db: AsyncSession = Depends(get_async_db),
48
48
  ) -> TaskLegacyReadV2:
49
49
  """
@@ -22,13 +22,13 @@ from ....schemas.v2 import WorkflowImportV2
22
22
  from ....schemas.v2 import WorkflowReadV2
23
23
  from ....schemas.v2 import WorkflowTaskCreateV2
24
24
  from ....schemas.v2 import WorkflowUpdateV2
25
- from ....security import current_active_user
26
- from ....security import User
27
25
  from ._aux_functions import _check_workflow_exists
28
26
  from ._aux_functions import _get_project_check_owner
29
27
  from ._aux_functions import _get_submitted_jobs_statement
30
28
  from ._aux_functions import _get_workflow_check_owner
31
29
  from ._aux_functions import _workflow_insert_task
30
+ from fractal_server.app.models import UserOAuth
31
+ from fractal_server.app.routes.auth import current_active_user
32
32
 
33
33
 
34
34
  router = APIRouter()
@@ -40,7 +40,7 @@ router = APIRouter()
40
40
  )
41
41
  async def get_workflow_list(
42
42
  project_id: int,
43
- user: User = Depends(current_active_user),
43
+ user: UserOAuth = Depends(current_active_user),
44
44
  db: AsyncSession = Depends(get_async_db),
45
45
  ) -> Optional[list[WorkflowReadV2]]:
46
46
  """
@@ -67,7 +67,7 @@ async def get_workflow_list(
67
67
  async def create_workflow(
68
68
  project_id: int,
69
69
  workflow: WorkflowCreateV2,
70
- user: User = Depends(current_active_user),
70
+ user: UserOAuth = Depends(current_active_user),
71
71
  db: AsyncSession = Depends(get_async_db),
72
72
  ) -> Optional[WorkflowReadV2]:
73
73
  """
@@ -95,7 +95,7 @@ async def create_workflow(
95
95
  async def read_workflow(
96
96
  project_id: int,
97
97
  workflow_id: int,
98
- user: User = Depends(current_active_user),
98
+ user: UserOAuth = Depends(current_active_user),
99
99
  db: AsyncSession = Depends(get_async_db),
100
100
  ) -> Optional[WorkflowReadV2]:
101
101
  """
@@ -120,7 +120,7 @@ async def update_workflow(
120
120
  project_id: int,
121
121
  workflow_id: int,
122
122
  patch: WorkflowUpdateV2,
123
- user: User = Depends(current_active_user),
123
+ user: UserOAuth = Depends(current_active_user),
124
124
  db: AsyncSession = Depends(get_async_db),
125
125
  ) -> Optional[WorkflowReadV2]:
126
126
  """
@@ -174,7 +174,7 @@ async def update_workflow(
174
174
  async def delete_workflow(
175
175
  project_id: int,
176
176
  workflow_id: int,
177
- user: User = Depends(current_active_user),
177
+ user: UserOAuth = Depends(current_active_user),
178
178
  db: AsyncSession = Depends(get_async_db),
179
179
  ) -> Response:
180
180
  """
@@ -227,7 +227,7 @@ async def delete_workflow(
227
227
  async def export_worfklow(
228
228
  project_id: int,
229
229
  workflow_id: int,
230
- user: User = Depends(current_active_user),
230
+ user: UserOAuth = Depends(current_active_user),
231
231
  db: AsyncSession = Depends(get_async_db),
232
232
  ) -> Optional[WorkflowExportV2]:
233
233
  """
@@ -273,7 +273,7 @@ async def export_worfklow(
273
273
  async def import_workflow(
274
274
  project_id: int,
275
275
  workflow: WorkflowImportV2,
276
- user: User = Depends(current_active_user),
276
+ user: UserOAuth = Depends(current_active_user),
277
277
  db: AsyncSession = Depends(get_async_db),
278
278
  ) -> Optional[WorkflowReadV2]:
279
279
  """
@@ -365,7 +365,7 @@ async def import_workflow(
365
365
 
366
366
  @router.get("/workflow/", response_model=list[WorkflowReadV2])
367
367
  async def get_user_workflows(
368
- user: User = Depends(current_active_user),
368
+ user: UserOAuth = Depends(current_active_user),
369
369
  db: AsyncSession = Depends(get_async_db),
370
370
  ) -> list[WorkflowReadV2]:
371
371
  """
@@ -373,7 +373,7 @@ async def get_user_workflows(
373
373
  """
374
374
  stm = select(WorkflowV2)
375
375
  stm = stm.join(ProjectV2).where(
376
- ProjectV2.user_list.any(User.id == user.id)
376
+ ProjectV2.user_list.any(UserOAuth.id == user.id)
377
377
  )
378
378
  res = await db.execute(stm)
379
379
  workflow_list = res.scalars().all()
@@ -14,11 +14,11 @@ from ....models.v2 import TaskV2
14
14
  from ....schemas.v2 import WorkflowTaskCreateV2
15
15
  from ....schemas.v2 import WorkflowTaskReadV2
16
16
  from ....schemas.v2 import WorkflowTaskUpdateV2
17
- from ....security import current_active_user
18
- from ....security import User
19
17
  from ._aux_functions import _get_workflow_check_owner
20
18
  from ._aux_functions import _get_workflow_task_check_owner
21
19
  from ._aux_functions import _workflow_insert_task
20
+ from fractal_server.app.models import UserOAuth
21
+ from fractal_server.app.routes.auth import current_active_user
22
22
 
23
23
  router = APIRouter()
24
24
 
@@ -33,7 +33,7 @@ async def create_workflowtask(
33
33
  workflow_id: int,
34
34
  task_id: int,
35
35
  new_task: WorkflowTaskCreateV2,
36
- user: User = Depends(current_active_user),
36
+ user: UserOAuth = Depends(current_active_user),
37
37
  db: AsyncSession = Depends(get_async_db),
38
38
  ) -> Optional[WorkflowTaskReadV2]:
39
39
  """
@@ -117,7 +117,7 @@ async def read_workflowtask(
117
117
  project_id: int,
118
118
  workflow_id: int,
119
119
  workflow_task_id: int,
120
- user: User = Depends(current_active_user),
120
+ user: UserOAuth = Depends(current_active_user),
121
121
  db: AsyncSession = Depends(get_async_db),
122
122
  ):
123
123
  workflow_task, _ = await _get_workflow_task_check_owner(
@@ -139,7 +139,7 @@ async def update_workflowtask(
139
139
  workflow_id: int,
140
140
  workflow_task_id: int,
141
141
  workflow_task_update: WorkflowTaskUpdateV2,
142
- user: User = Depends(current_active_user),
142
+ user: UserOAuth = Depends(current_active_user),
143
143
  db: AsyncSession = Depends(get_async_db),
144
144
  ) -> Optional[WorkflowTaskReadV2]:
145
145
  """
@@ -223,7 +223,7 @@ async def delete_workflowtask(
223
223
  project_id: int,
224
224
  workflow_id: int,
225
225
  workflow_task_id: int,
226
- user: User = Depends(current_active_user),
226
+ user: UserOAuth = Depends(current_active_user),
227
227
  db: AsyncSession = Depends(get_async_db),
228
228
  ) -> Response:
229
229
  """
@@ -0,0 +1,55 @@
1
+ from fastapi_users import FastAPIUsers
2
+ from fastapi_users.authentication import AuthenticationBackend
3
+ from fastapi_users.authentication import BearerTransport
4
+ from fastapi_users.authentication import CookieTransport
5
+ from fastapi_users.authentication import JWTStrategy
6
+
7
+ from fractal_server.app.models import UserOAuth
8
+ from fractal_server.app.security import get_user_manager
9
+ from fractal_server.config import get_settings
10
+ from fractal_server.syringe import Inject
11
+
12
+
13
+ bearer_transport = BearerTransport(tokenUrl="/auth/token/login")
14
+ cookie_transport = CookieTransport(cookie_samesite="none")
15
+
16
+
17
+ def get_jwt_strategy() -> JWTStrategy:
18
+ settings = Inject(get_settings)
19
+ return JWTStrategy(
20
+ secret=settings.JWT_SECRET_KEY, # type: ignore
21
+ lifetime_seconds=settings.JWT_EXPIRE_SECONDS,
22
+ )
23
+
24
+
25
+ def get_jwt_cookie_strategy() -> JWTStrategy:
26
+ settings = Inject(get_settings)
27
+ return JWTStrategy(
28
+ secret=settings.JWT_SECRET_KEY, # type: ignore
29
+ lifetime_seconds=settings.COOKIE_EXPIRE_SECONDS,
30
+ )
31
+
32
+
33
+ token_backend = AuthenticationBackend(
34
+ name="bearer-jwt",
35
+ transport=bearer_transport,
36
+ get_strategy=get_jwt_strategy,
37
+ )
38
+ cookie_backend = AuthenticationBackend(
39
+ name="cookie-jwt",
40
+ transport=cookie_transport,
41
+ get_strategy=get_jwt_cookie_strategy,
42
+ )
43
+
44
+
45
+ fastapi_users = FastAPIUsers[UserOAuth, int](
46
+ get_user_manager,
47
+ [token_backend, cookie_backend],
48
+ )
49
+ current_active_user = fastapi_users.current_user(active=True)
50
+ current_active_verified_user = fastapi_users.current_user(
51
+ active=True, verified=True
52
+ )
53
+ current_active_superuser = fastapi_users.current_user(
54
+ active=True, superuser=True
55
+ )
@@ -0,0 +1,107 @@
1
+ from fastapi import HTTPException
2
+ from fastapi import status
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlmodel import select
5
+
6
+ from ...models.linkusergroup import LinkUserGroup
7
+ from ...models.security import UserGroup
8
+ from ...models.security import UserOAuth
9
+ from ...schemas.user import UserRead
10
+ from ...schemas.user_group import UserGroupRead
11
+
12
+
13
+ async def _get_single_user_with_group_names(
14
+ user: UserOAuth,
15
+ db: AsyncSession,
16
+ ) -> UserRead:
17
+ """
18
+ Enrich a user object by filling its `group_names` attribute.
19
+
20
+ Arguments:
21
+ user: The current `UserOAuth` object
22
+ db: Async db session
23
+
24
+ Returns:
25
+ A `UserRead` object with `group_names` set
26
+ """
27
+ stm_groups = (
28
+ select(UserGroup)
29
+ .join(LinkUserGroup)
30
+ .where(LinkUserGroup.user_id == UserOAuth.id)
31
+ )
32
+ res = await db.execute(stm_groups)
33
+ groups = res.scalars().unique().all()
34
+ group_names = [group.name for group in groups]
35
+ return UserRead(**user.model_dump(), group_names=group_names)
36
+
37
+
38
+ async def _get_single_user_with_group_ids(
39
+ user: UserOAuth,
40
+ db: AsyncSession,
41
+ ) -> UserRead:
42
+ """
43
+ Enrich a user object by filling its `group_ids` attribute.
44
+
45
+ Arguments:
46
+ user: The current `UserOAuth` object
47
+ db: Async db session
48
+
49
+ Returns:
50
+ A `UserRead` object with `group_ids` set
51
+ """
52
+ stm_links = select(LinkUserGroup).where(LinkUserGroup.user_id == user.id)
53
+ res = await db.execute(stm_links)
54
+ links = res.scalars().unique().all()
55
+ group_ids = [link.group_id for link in links]
56
+ return UserRead(**user.model_dump(), group_ids=group_ids)
57
+
58
+
59
+ async def _get_single_group_with_user_ids(
60
+ group_id: int, db: AsyncSession
61
+ ) -> UserGroupRead:
62
+ """
63
+ Get a group, and construct its `user_ids` list.
64
+
65
+ Arguments:
66
+ group_id:
67
+ db:
68
+
69
+ Returns:
70
+ `UserGroupRead` object, with `user_ids` attribute populated
71
+ from database.
72
+ """
73
+ # Get the UserGroup object from the database
74
+ stm_group = select(UserGroup).where(UserGroup.id == group_id)
75
+ res = await db.execute(stm_group)
76
+ group = res.scalars().one_or_none()
77
+ if group is None:
78
+ raise HTTPException(
79
+ status_code=status.HTTP_404_NOT_FOUND,
80
+ detail=f"Group {group_id} not found.",
81
+ )
82
+
83
+ # Get all user/group links
84
+ stm_links = select(LinkUserGroup).where(LinkUserGroup.group_id == group_id)
85
+ res = await db.execute(stm_links)
86
+ links = res.scalars().all()
87
+ user_ids = [link.user_id for link in links]
88
+
89
+ return UserGroupRead(**group.model_dump(), user_ids=user_ids)
90
+
91
+
92
+ async def _user_or_404(user_id: int, db: AsyncSession) -> UserOAuth:
93
+ """
94
+ Get a user from db, or raise a 404 HTTP exception if missing.
95
+
96
+ Arguments:
97
+ user_id: ID of the user
98
+ db: Async db session
99
+ """
100
+ stm = select(UserOAuth).where(UserOAuth.id == user_id)
101
+ res = await db.execute(stm)
102
+ user = res.scalars().unique().one_or_none()
103
+ if user is None:
104
+ raise HTTPException(
105
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found."
106
+ )
107
+ return user
@@ -0,0 +1,60 @@
1
+ """
2
+ Definition of `/auth/current-user/` endpoints
3
+ """
4
+ from fastapi import APIRouter
5
+ from fastapi import Depends
6
+ from fastapi_users import schemas
7
+ from sqlalchemy.ext.asyncio import AsyncSession
8
+
9
+ from . import current_active_user
10
+ from ...db import get_async_db
11
+ from ...schemas.user import UserRead
12
+ from ...schemas.user import UserUpdate
13
+ from ...schemas.user import UserUpdateStrict
14
+ from ._aux_auth import _get_single_user_with_group_names
15
+ from fractal_server.app.models import UserOAuth
16
+ from fractal_server.app.security import get_user_manager
17
+ from fractal_server.app.security import UserManager
18
+
19
+ router_current_user = APIRouter()
20
+
21
+
22
+ @router_current_user.get("/current-user/", response_model=UserRead)
23
+ async def get_current_user(
24
+ group_names: bool = False,
25
+ user: UserOAuth = Depends(current_active_user),
26
+ db: AsyncSession = Depends(get_async_db),
27
+ ):
28
+ """
29
+ Return current user
30
+ """
31
+ if group_names is True:
32
+ user_with_groups = await _get_single_user_with_group_names(user, db)
33
+ return user_with_groups
34
+ else:
35
+ return user
36
+
37
+
38
+ @router_current_user.patch("/current-user/", response_model=UserRead)
39
+ async def patch_current_user(
40
+ user_update: UserUpdateStrict,
41
+ current_user: UserOAuth = Depends(current_active_user),
42
+ user_manager: UserManager = Depends(get_user_manager),
43
+ db: AsyncSession = Depends(get_async_db),
44
+ ):
45
+ """
46
+ Note: a user cannot patch their own password (as enforced within the
47
+ `UserUpdateStrict` schema).
48
+ """
49
+ update = UserUpdate(**user_update.dict(exclude_unset=True))
50
+
51
+ # NOTE: here it would be relevant to catch an `InvalidPasswordException`
52
+ # (from `fastapi_users.exceptions`), if we were to allow users change
53
+ # their own password
54
+ user = await user_manager.update(update, current_user, safe=True)
55
+ patched_user = schemas.model_validate(UserOAuth, user)
56
+
57
+ patched_user_with_groups = await _get_single_user_with_group_names(
58
+ patched_user, db
59
+ )
60
+ return patched_user_with_groups
@@ -0,0 +1,159 @@
1
+ """
2
+ Definition of `/auth/group/` routes
3
+ """
4
+ from fastapi import APIRouter
5
+ from fastapi import Depends
6
+ from fastapi import HTTPException
7
+ from fastapi import status
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from sqlmodel import col
10
+ from sqlmodel import func
11
+ from sqlmodel import select
12
+
13
+ from . import current_active_superuser
14
+ from ...db import get_async_db
15
+ from ...schemas.user_group import UserGroupCreate
16
+ from ...schemas.user_group import UserGroupRead
17
+ from ...schemas.user_group import UserGroupUpdate
18
+ from ._aux_auth import _get_single_group_with_user_ids
19
+ from fractal_server.app.models import LinkUserGroup
20
+ from fractal_server.app.models import UserGroup
21
+ from fractal_server.app.models import UserOAuth
22
+
23
+ router_group = APIRouter()
24
+
25
+
26
+ @router_group.get(
27
+ "/group/", response_model=list[UserGroupRead], status_code=200
28
+ )
29
+ async def get_list_user_groups(
30
+ user_ids: bool = False,
31
+ user: UserOAuth = Depends(current_active_superuser),
32
+ db: AsyncSession = Depends(get_async_db),
33
+ ) -> list[UserGroupRead]:
34
+
35
+ # Get all groups
36
+ stm_all_groups = select(UserGroup)
37
+ res = await db.execute(stm_all_groups)
38
+ groups = res.scalars().all()
39
+
40
+ if user_ids is True:
41
+ # Get all user/group links
42
+ stm_all_links = select(LinkUserGroup)
43
+ res = await db.execute(stm_all_links)
44
+ links = res.scalars().all()
45
+
46
+ # TODO: possible optimizations for this construction are listed in
47
+ # https://github.com/fractal-analytics-platform/fractal-server/issues/1742
48
+ for ind, group in enumerate(groups):
49
+ groups[ind] = dict(
50
+ group.model_dump(),
51
+ user_ids=[
52
+ link.user_id for link in links if link.group_id == group.id
53
+ ],
54
+ )
55
+
56
+ return groups
57
+
58
+
59
+ @router_group.get(
60
+ "/group/{group_id}/",
61
+ response_model=UserGroupRead,
62
+ status_code=status.HTTP_200_OK,
63
+ )
64
+ async def get_single_user_group(
65
+ group_id: int,
66
+ user: UserOAuth = Depends(current_active_superuser),
67
+ db: AsyncSession = Depends(get_async_db),
68
+ ) -> UserGroupRead:
69
+ group = await _get_single_group_with_user_ids(group_id=group_id, db=db)
70
+ return group
71
+
72
+
73
+ @router_group.post(
74
+ "/group/",
75
+ response_model=UserGroupRead,
76
+ status_code=status.HTTP_201_CREATED,
77
+ )
78
+ async def create_single_group(
79
+ group_create: UserGroupCreate,
80
+ user: UserOAuth = Depends(current_active_superuser),
81
+ db: AsyncSession = Depends(get_async_db),
82
+ ) -> UserGroupRead:
83
+
84
+ # Check that name is not already in use
85
+ existing_name_str = select(UserGroup).where(
86
+ UserGroup.name == group_create.name
87
+ )
88
+ res = await db.execute(existing_name_str)
89
+ group = res.scalars().one_or_none()
90
+ if group is not None:
91
+ raise HTTPException(
92
+ status_code=422, detail="A group with the same name already exists"
93
+ )
94
+
95
+ # Create and return new group
96
+ new_group = UserGroup(name=group_create.name)
97
+ db.add(new_group)
98
+ await db.commit()
99
+
100
+ return dict(new_group.model_dump(), user_ids=[])
101
+
102
+
103
+ @router_group.patch(
104
+ "/group/{group_id}/",
105
+ response_model=UserGroupRead,
106
+ status_code=status.HTTP_200_OK,
107
+ )
108
+ async def update_single_group(
109
+ group_id: int,
110
+ group_update: UserGroupUpdate,
111
+ user: UserOAuth = Depends(current_active_superuser),
112
+ db: AsyncSession = Depends(get_async_db),
113
+ ) -> UserGroupRead:
114
+
115
+ # Check that all required users exist
116
+ # Note: The reason for introducing `col` is as in
117
+ # https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
118
+ stm = select(func.count()).where(
119
+ col(UserOAuth.id).in_(group_update.new_user_ids)
120
+ )
121
+ res = await db.execute(stm)
122
+ number_matching_users = res.scalar()
123
+ if number_matching_users != len(group_update.new_user_ids):
124
+ raise HTTPException(
125
+ status_code=status.HTTP_404_NOT_FOUND,
126
+ detail=(
127
+ f"Not all requested users (IDs {group_update.new_user_ids}) "
128
+ "exist."
129
+ ),
130
+ )
131
+
132
+ # Add new users to existing group
133
+ for user_id in group_update.new_user_ids:
134
+ link = LinkUserGroup(user_id=user_id, group_id=group_id)
135
+ db.add(link)
136
+ await db.commit()
137
+
138
+ updated_group = await _get_single_group_with_user_ids(
139
+ group_id=group_id, db=db
140
+ )
141
+
142
+ return updated_group
143
+
144
+
145
+ @router_group.delete(
146
+ "/group/{group_id}/", status_code=status.HTTP_405_METHOD_NOT_ALLOWED
147
+ )
148
+ async def delete_single_group(
149
+ group_id: int,
150
+ user: UserOAuth = Depends(current_active_superuser),
151
+ db: AsyncSession = Depends(get_async_db),
152
+ ) -> UserGroupRead:
153
+ raise HTTPException(
154
+ status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
155
+ detail=(
156
+ "Deleting a user group is not allowed, as it may restrict "
157
+ "previously-granted access.",
158
+ ),
159
+ )