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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +25 -2
- fractal_server/app/models/__init__.py +11 -5
- fractal_server/app/models/linkusergroup.py +11 -0
- fractal_server/app/models/security.py +24 -3
- fractal_server/app/models/v1/project.py +1 -1
- fractal_server/app/models/v2/project.py +3 -3
- fractal_server/app/routes/admin/v1.py +14 -14
- fractal_server/app/routes/admin/v2.py +12 -12
- fractal_server/app/routes/api/__init__.py +2 -2
- fractal_server/app/routes/api/v1/_aux_functions.py +2 -2
- fractal_server/app/routes/api/v1/dataset.py +17 -15
- fractal_server/app/routes/api/v1/job.py +11 -9
- fractal_server/app/routes/api/v1/project.py +9 -9
- fractal_server/app/routes/api/v1/task.py +8 -8
- fractal_server/app/routes/api/v1/task_collection.py +5 -5
- fractal_server/app/routes/api/v1/workflow.py +13 -11
- fractal_server/app/routes/api/v1/workflowtask.py +6 -6
- fractal_server/app/routes/api/v2/_aux_functions.py +2 -2
- fractal_server/app/routes/api/v2/dataset.py +11 -11
- fractal_server/app/routes/api/v2/images.py +6 -6
- fractal_server/app/routes/api/v2/job.py +9 -9
- fractal_server/app/routes/api/v2/project.py +7 -7
- fractal_server/app/routes/api/v2/status.py +3 -3
- fractal_server/app/routes/api/v2/submit.py +3 -3
- fractal_server/app/routes/api/v2/task.py +8 -8
- fractal_server/app/routes/api/v2/task_collection.py +5 -5
- fractal_server/app/routes/api/v2/task_collection_custom.py +3 -3
- fractal_server/app/routes/api/v2/task_legacy.py +9 -9
- fractal_server/app/routes/api/v2/workflow.py +11 -11
- fractal_server/app/routes/api/v2/workflowtask.py +6 -6
- fractal_server/app/routes/auth/__init__.py +55 -0
- fractal_server/app/routes/auth/_aux_auth.py +107 -0
- fractal_server/app/routes/auth/current_user.py +60 -0
- fractal_server/app/routes/auth/group.py +159 -0
- fractal_server/app/routes/auth/group_names.py +34 -0
- fractal_server/app/routes/auth/login.py +25 -0
- fractal_server/app/routes/auth/oauth.py +63 -0
- fractal_server/app/routes/auth/register.py +23 -0
- fractal_server/app/routes/auth/router.py +19 -0
- fractal_server/app/routes/auth/users.py +173 -0
- fractal_server/app/schemas/user.py +7 -0
- fractal_server/app/schemas/user_group.py +57 -0
- fractal_server/app/security/__init__.py +72 -75
- fractal_server/data_migrations/2_4_0.py +61 -0
- fractal_server/main.py +1 -9
- fractal_server/migrations/versions/091b01f51f88_add_usergroup_and_linkusergroup_table.py +53 -0
- {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/METADATA +1 -1
- {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/RECORD +52 -39
- fractal_server/app/routes/auth.py +0 -165
- {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/LICENSE +0 -0
- {fractal_server-2.3.11.dist-info → fractal_server-2.4.0a1.dist-info}/WHEEL +0 -0
- {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:
|
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:
|
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:
|
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:
|
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:
|
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
|
29
|
-
from
|
30
|
-
from
|
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:
|
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:
|
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
|
22
|
-
from
|
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:
|
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
|
8
|
-
from
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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(
|
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:
|
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:
|
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:
|
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:
|
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
|
+
)
|