fractal-server 2.9.0a10__py3-none-any.whl → 2.9.0a11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fractal_server/__init__.py +1 -1
- fractal_server/app/routes/auth/users.py +97 -99
- fractal_server/app/schemas/user.py +16 -10
- {fractal_server-2.9.0a10.dist-info → fractal_server-2.9.0a11.dist-info}/METADATA +1 -1
- {fractal_server-2.9.0a10.dist-info → fractal_server-2.9.0a11.dist-info}/RECORD +8 -8
- {fractal_server-2.9.0a10.dist-info → fractal_server-2.9.0a11.dist-info}/LICENSE +0 -0
- {fractal_server-2.9.0a10.dist-info → fractal_server-2.9.0a11.dist-info}/WHEEL +0 -0
- {fractal_server-2.9.0a10.dist-info → fractal_server-2.9.0a11.dist-info}/entry_points.txt +0 -0
fractal_server/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "2.9.
|
1
|
+
__VERSION__ = "2.9.0a11"
|
@@ -8,9 +8,7 @@ from fastapi import status
|
|
8
8
|
from fastapi_users import exceptions
|
9
9
|
from fastapi_users import schemas
|
10
10
|
from fastapi_users.router.common import ErrorCode
|
11
|
-
from sqlalchemy.exc import IntegrityError
|
12
11
|
from sqlalchemy.ext.asyncio import AsyncSession
|
13
|
-
from sqlmodel import col
|
14
12
|
from sqlmodel import func
|
15
13
|
from sqlmodel import select
|
16
14
|
|
@@ -18,9 +16,10 @@ from . import current_active_superuser
|
|
18
16
|
from ...db import get_async_db
|
19
17
|
from ...schemas.user import UserRead
|
20
18
|
from ...schemas.user import UserUpdate
|
21
|
-
from ...schemas.user import UserUpdateWithNewGroupIds
|
22
19
|
from ..aux.validate_user_settings import verify_user_has_settings
|
20
|
+
from ._aux_auth import _get_default_usergroup_id
|
23
21
|
from ._aux_auth import _get_single_user_with_groups
|
22
|
+
from ._aux_auth import FRACTAL_DEFAULT_GROUP_NAME
|
24
23
|
from fractal_server.app.models import LinkUserGroup
|
25
24
|
from fractal_server.app.models import UserGroup
|
26
25
|
from fractal_server.app.models import UserOAuth
|
@@ -28,6 +27,7 @@ from fractal_server.app.models import UserSettings
|
|
28
27
|
from fractal_server.app.routes.auth._aux_auth import _user_or_404
|
29
28
|
from fractal_server.app.schemas import UserSettingsRead
|
30
29
|
from fractal_server.app.schemas import UserSettingsUpdate
|
30
|
+
from fractal_server.app.schemas.user import UserUpdateGroups
|
31
31
|
from fractal_server.app.security import get_user_manager
|
32
32
|
from fractal_server.app.security import UserManager
|
33
33
|
from fractal_server.logger import set_logger
|
@@ -55,114 +55,43 @@ async def get_user(
|
|
55
55
|
@router_users.patch("/users/{user_id}/", response_model=UserRead)
|
56
56
|
async def patch_user(
|
57
57
|
user_id: int,
|
58
|
-
user_update:
|
58
|
+
user_update: UserUpdate,
|
59
59
|
current_superuser: UserOAuth = Depends(current_active_superuser),
|
60
60
|
user_manager: UserManager = Depends(get_user_manager),
|
61
61
|
db: AsyncSession = Depends(get_async_db),
|
62
62
|
):
|
63
63
|
"""
|
64
64
|
Custom version of the PATCH-user route from `fastapi-users`.
|
65
|
-
|
66
|
-
In order to keep the fastapi-users logic in place (which is convenient to
|
67
|
-
update user attributes), we split the endpoint into two branches. We either
|
68
|
-
go through the fastapi-users-based attribute-update branch, or through the
|
69
|
-
branch where we establish new user/group relationships.
|
70
|
-
|
71
|
-
Note that we prevent making both changes at the same time, since it would
|
72
|
-
be more complex to guarantee that endpoint error would leave the database
|
73
|
-
in the same state as before the API call.
|
74
65
|
"""
|
75
66
|
|
76
|
-
# We prevent simultaneous editing of both user attributes and user/group
|
77
|
-
# associations
|
78
|
-
user_update_dict_without_groups = user_update.dict(
|
79
|
-
exclude_unset=True, exclude={"new_group_ids"}
|
80
|
-
)
|
81
|
-
edit_attributes = user_update_dict_without_groups != {}
|
82
|
-
edit_groups = user_update.new_group_ids is not None
|
83
|
-
if edit_attributes and edit_groups:
|
84
|
-
raise HTTPException(
|
85
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
86
|
-
detail=(
|
87
|
-
"Cannot modify both user attributes and group membership. "
|
88
|
-
"Please make two independent PATCH calls"
|
89
|
-
),
|
90
|
-
)
|
91
|
-
|
92
67
|
# Check that user exists
|
93
68
|
user_to_patch = await _user_or_404(user_id, db)
|
94
69
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
70
|
+
# Modify user attributes
|
71
|
+
try:
|
72
|
+
user = await user_manager.update(
|
73
|
+
user_update,
|
74
|
+
user_to_patch,
|
75
|
+
safe=False,
|
76
|
+
request=None,
|
77
|
+
)
|
78
|
+
validated_user = schemas.model_validate(UserOAuth, user)
|
79
|
+
patched_user = await db.get(
|
80
|
+
UserOAuth, validated_user.id, populate_existing=True
|
81
|
+
)
|
82
|
+
except exceptions.InvalidPasswordException as e:
|
83
|
+
raise HTTPException(
|
84
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
85
|
+
detail={
|
86
|
+
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
87
|
+
"reason": e.reason,
|
88
|
+
},
|
89
|
+
)
|
90
|
+
except exceptions.UserAlreadyExists:
|
91
|
+
raise HTTPException(
|
92
|
+
status.HTTP_400_BAD_REQUEST,
|
93
|
+
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
|
103
94
|
)
|
104
|
-
res = await db.execute(stm)
|
105
|
-
number_matching_groups = res.scalar()
|
106
|
-
if number_matching_groups != len(user_update.new_group_ids):
|
107
|
-
raise HTTPException(
|
108
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
109
|
-
detail=(
|
110
|
-
"Not all requested groups (IDs: "
|
111
|
-
f"{user_update.new_group_ids}) exist."
|
112
|
-
),
|
113
|
-
)
|
114
|
-
|
115
|
-
for new_group_id in user_update.new_group_ids:
|
116
|
-
link = LinkUserGroup(user_id=user_id, group_id=new_group_id)
|
117
|
-
db.add(link)
|
118
|
-
|
119
|
-
try:
|
120
|
-
await db.commit()
|
121
|
-
except IntegrityError as e:
|
122
|
-
error_msg = (
|
123
|
-
f"Cannot link groups with IDs {user_update.new_group_ids} "
|
124
|
-
f"to user {user_id}. "
|
125
|
-
"Likely reason: one of these links already exists.\n"
|
126
|
-
f"Original error: {str(e)}"
|
127
|
-
)
|
128
|
-
logger.info(error_msg)
|
129
|
-
raise HTTPException(
|
130
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
131
|
-
detail=error_msg,
|
132
|
-
)
|
133
|
-
patched_user = user_to_patch
|
134
|
-
elif edit_attributes:
|
135
|
-
# Modify user attributes
|
136
|
-
try:
|
137
|
-
user_update_without_groups = UserUpdate(
|
138
|
-
**user_update_dict_without_groups
|
139
|
-
)
|
140
|
-
user = await user_manager.update(
|
141
|
-
user_update_without_groups,
|
142
|
-
user_to_patch,
|
143
|
-
safe=False,
|
144
|
-
request=None,
|
145
|
-
)
|
146
|
-
validated_user = schemas.model_validate(UserOAuth, user)
|
147
|
-
patched_user = await db.get(
|
148
|
-
UserOAuth, validated_user.id, populate_existing=True
|
149
|
-
)
|
150
|
-
except exceptions.InvalidPasswordException as e:
|
151
|
-
raise HTTPException(
|
152
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
153
|
-
detail={
|
154
|
-
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
155
|
-
"reason": e.reason,
|
156
|
-
},
|
157
|
-
)
|
158
|
-
except exceptions.UserAlreadyExists:
|
159
|
-
raise HTTPException(
|
160
|
-
status.HTTP_400_BAD_REQUEST,
|
161
|
-
detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
|
162
|
-
)
|
163
|
-
else:
|
164
|
-
# Nothing to do, just continue
|
165
|
-
patched_user = user_to_patch
|
166
95
|
|
167
96
|
# Enrich user object with `group_ids_names` attribute
|
168
97
|
patched_user_with_groups = await _get_single_user_with_groups(
|
@@ -203,6 +132,75 @@ async def list_users(
|
|
203
132
|
return user_list
|
204
133
|
|
205
134
|
|
135
|
+
@router_users.post("/users/{user_id}/set-groups/", response_model=UserRead)
|
136
|
+
async def set_user_groups(
|
137
|
+
user_id: int,
|
138
|
+
user_update: UserUpdateGroups,
|
139
|
+
superuser: UserOAuth = Depends(current_active_superuser),
|
140
|
+
db: AsyncSession = Depends(get_async_db),
|
141
|
+
) -> UserRead:
|
142
|
+
|
143
|
+
# Preliminary check that all objects exist in the db
|
144
|
+
user = await _user_or_404(user_id=user_id, db=db)
|
145
|
+
target_group_ids = user_update.group_ids
|
146
|
+
stm = select(func.count(UserGroup.id)).where(
|
147
|
+
UserGroup.id.in_(target_group_ids)
|
148
|
+
)
|
149
|
+
res = await db.execute(stm)
|
150
|
+
count = res.scalar()
|
151
|
+
if count != len(target_group_ids):
|
152
|
+
raise HTTPException(
|
153
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
154
|
+
detail=f"Some UserGroups in {target_group_ids} do not exist.",
|
155
|
+
)
|
156
|
+
|
157
|
+
# Check that default group is not being removed
|
158
|
+
default_group_id = await _get_default_usergroup_id(db=db)
|
159
|
+
if default_group_id not in target_group_ids:
|
160
|
+
raise HTTPException(
|
161
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
162
|
+
detail=(
|
163
|
+
f"Cannot remove user from "
|
164
|
+
f"'{FRACTAL_DEFAULT_GROUP_NAME}' group.",
|
165
|
+
),
|
166
|
+
)
|
167
|
+
|
168
|
+
# Prepare lists of links to be removed
|
169
|
+
res = await db.execute(
|
170
|
+
select(LinkUserGroup)
|
171
|
+
.where(LinkUserGroup.user_id == user_id)
|
172
|
+
.where(LinkUserGroup.group_id.not_in(target_group_ids))
|
173
|
+
)
|
174
|
+
links_to_remove = res.scalars().all()
|
175
|
+
|
176
|
+
# Prepare lists of links to be added
|
177
|
+
res = await db.execute(
|
178
|
+
select(LinkUserGroup.group_id)
|
179
|
+
.where(LinkUserGroup.user_id == user_id)
|
180
|
+
.where(LinkUserGroup.group_id.in_(target_group_ids))
|
181
|
+
)
|
182
|
+
ids_links_already_in = res.scalars().all()
|
183
|
+
ids_links_to_add = set(target_group_ids) - set(ids_links_already_in)
|
184
|
+
|
185
|
+
# Remove/create links as needed
|
186
|
+
for link in links_to_remove:
|
187
|
+
logger.info(
|
188
|
+
f"Removing LinkUserGroup with {link.user_id=} "
|
189
|
+
f"and {link.group_id=}."
|
190
|
+
)
|
191
|
+
await db.delete(link)
|
192
|
+
for group_id in ids_links_to_add:
|
193
|
+
logger.info(
|
194
|
+
f"Creating new LinkUserGroup with {user_id=} " f"and {group_id=}."
|
195
|
+
)
|
196
|
+
db.add(LinkUserGroup(user_id=user_id, group_id=group_id))
|
197
|
+
await db.commit()
|
198
|
+
|
199
|
+
user_with_groups = await _get_single_user_with_groups(user, db)
|
200
|
+
|
201
|
+
return user_with_groups
|
202
|
+
|
203
|
+
|
206
204
|
@router_users.get(
|
207
205
|
"/users/{user_id}/settings/", response_model=UserSettingsRead
|
208
206
|
)
|
@@ -3,6 +3,7 @@ from typing import Optional
|
|
3
3
|
from fastapi_users import schemas
|
4
4
|
from pydantic import BaseModel
|
5
5
|
from pydantic import Extra
|
6
|
+
from pydantic import Field
|
6
7
|
from pydantic import validator
|
7
8
|
|
8
9
|
from ._validators import val_unique_list
|
@@ -11,8 +12,8 @@ from ._validators import valstr
|
|
11
12
|
__all__ = (
|
12
13
|
"UserRead",
|
13
14
|
"UserUpdate",
|
15
|
+
"UserUpdateGroups",
|
14
16
|
"UserCreate",
|
15
|
-
"UserUpdateWithNewGroupIds",
|
16
17
|
)
|
17
18
|
|
18
19
|
|
@@ -45,7 +46,7 @@ class UserRead(schemas.BaseUser[int]):
|
|
45
46
|
oauth_accounts: list[OAuthAccountRead]
|
46
47
|
|
47
48
|
|
48
|
-
class UserUpdate(schemas.BaseUserUpdate):
|
49
|
+
class UserUpdate(schemas.BaseUserUpdate, extra=Extra.forbid):
|
49
50
|
"""
|
50
51
|
Schema for `User` update.
|
51
52
|
|
@@ -82,14 +83,6 @@ class UserUpdateStrict(BaseModel, extra=Extra.forbid):
|
|
82
83
|
pass
|
83
84
|
|
84
85
|
|
85
|
-
class UserUpdateWithNewGroupIds(UserUpdate):
|
86
|
-
new_group_ids: Optional[list[int]] = None
|
87
|
-
|
88
|
-
_val_unique = validator("new_group_ids", allow_reuse=True)(
|
89
|
-
val_unique_list("new_group_ids")
|
90
|
-
)
|
91
|
-
|
92
|
-
|
93
86
|
class UserCreate(schemas.BaseUserCreate):
|
94
87
|
"""
|
95
88
|
Schema for `User` creation.
|
@@ -103,3 +96,16 @@ class UserCreate(schemas.BaseUserCreate):
|
|
103
96
|
# Validators
|
104
97
|
|
105
98
|
_username = validator("username", allow_reuse=True)(valstr("username"))
|
99
|
+
|
100
|
+
|
101
|
+
class UserUpdateGroups(BaseModel, extra=Extra.forbid):
|
102
|
+
"""
|
103
|
+
Schema for `POST /auth/users/{user_id}/set-groups/`
|
104
|
+
|
105
|
+
"""
|
106
|
+
|
107
|
+
group_ids: list[int] = Field(min_items=1)
|
108
|
+
|
109
|
+
_group_ids = validator("group_ids", allow_reuse=True)(
|
110
|
+
val_unique_list("group_ids")
|
111
|
+
)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
fractal_server/__init__.py,sha256=
|
1
|
+
fractal_server/__init__.py,sha256=awxUTr6llNkjH1-5c-_yED0mpNs4APblXMX5R7fA0Qs,25
|
2
2
|
fractal_server/__main__.py,sha256=dEkCfzLLQrIlxsGC-HBfoR-RBMWnJDgNrxYTyzmE9c0,6146
|
3
3
|
fractal_server/alembic.ini,sha256=MWwi7GzjzawI9cCAK1LW7NxIBQDUqD12-ptJoq5JpP0,3153
|
4
4
|
fractal_server/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -68,7 +68,7 @@ fractal_server/app/routes/auth/login.py,sha256=tSu6OBLOieoBtMZB4JkBAdEgH2Y8KqPGS
|
|
68
68
|
fractal_server/app/routes/auth/oauth.py,sha256=AnFHbjqL2AgBX3eksI931xD6RTtmbciHBEuGf9YJLjU,1895
|
69
69
|
fractal_server/app/routes/auth/register.py,sha256=DlHq79iOvGd_gt2v9uwtsqIKeO6i_GKaW59VIkllPqY,587
|
70
70
|
fractal_server/app/routes/auth/router.py,sha256=tzJrygXFZlmV_uWelVqTOJMEH-3Fr7ydwlgx1LxRjxY,527
|
71
|
-
fractal_server/app/routes/auth/users.py,sha256=
|
71
|
+
fractal_server/app/routes/auth/users.py,sha256=kZv-Ls224WBFiuvVeM584LhYq_BLz6HQ9HpWbWQxRRM,7808
|
72
72
|
fractal_server/app/routes/aux/__init__.py,sha256=LR4bR7RunHAK6jc9IR2bReQd-BdXADdnDccXI4uGeGY,731
|
73
73
|
fractal_server/app/routes/aux/_job.py,sha256=q-RCiW17yXnZKAC_0La52RLvhqhxuvbgQJ2MlGXOj8A,702
|
74
74
|
fractal_server/app/routes/aux/_runner.py,sha256=FdCVla5DxGAZ__aB7Z8dEJzD_RIeh5tftjrPyqkr8N8,895
|
@@ -135,7 +135,7 @@ fractal_server/app/runner/v2/task_interface.py,sha256=hT3p-bRGsLNAR_dNv_PYFoqzIF
|
|
135
135
|
fractal_server/app/runner/versions.py,sha256=dSaPRWqmFPHjg20kTCHmi_dmGNcCETflDtDLronNanU,852
|
136
136
|
fractal_server/app/schemas/__init__.py,sha256=stURAU_t3AOBaH0HSUbV-GKhlPKngnnIMoqWc3orFyI,135
|
137
137
|
fractal_server/app/schemas/_validators.py,sha256=T5EswIJAJRvawfzqWtPcN2INAfiBXyE4m0iwQm4ht-0,3149
|
138
|
-
fractal_server/app/schemas/user.py,sha256=
|
138
|
+
fractal_server/app/schemas/user.py,sha256=icjox9gK_invW44Nh_L4CvqfRa92qghyQhmevyg09nQ,2243
|
139
139
|
fractal_server/app/schemas/user_group.py,sha256=t30Kd07PY43G_AqFDb8vjdInTeLeU9WvFZDx8fVLPSI,1750
|
140
140
|
fractal_server/app/schemas/user_settings.py,sha256=TalISeEfCrtN8LgqbLx1Q8ZPoeiZnbksg5NYAVzkIqY,3527
|
141
141
|
fractal_server/app/schemas/v1/__init__.py,sha256=CrBGgBhoemCvmZ70ZUchM-jfVAICnoa7AjZBAtL2UB0,1852
|
@@ -238,8 +238,8 @@ fractal_server/tasks/v2/utils_templates.py,sha256=C5WLuY3uGG2s53OEL-__H35-fmSlgu
|
|
238
238
|
fractal_server/urls.py,sha256=5o_qq7PzKKbwq12NHSQZDmDitn5RAOeQ4xufu-2v9Zk,448
|
239
239
|
fractal_server/utils.py,sha256=utvmBx8K9I8hRWFquxna2pBaOqe0JifDL_NVPmihEJI,3525
|
240
240
|
fractal_server/zip_tools.py,sha256=GjDgo_sf6V_DDg6wWeBlZu5zypIxycn_l257p_YVKGc,4876
|
241
|
-
fractal_server-2.9.
|
242
|
-
fractal_server-2.9.
|
243
|
-
fractal_server-2.9.
|
244
|
-
fractal_server-2.9.
|
245
|
-
fractal_server-2.9.
|
241
|
+
fractal_server-2.9.0a11.dist-info/LICENSE,sha256=QKAharUuhxL58kSoLizKJeZE3mTCBnX6ucmz8W0lxlk,1576
|
242
|
+
fractal_server-2.9.0a11.dist-info/METADATA,sha256=0SUdcGO7gPL9_VuNTLt10VitKNIgZTgQ5VL4FV2xjX8,4586
|
243
|
+
fractal_server-2.9.0a11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
244
|
+
fractal_server-2.9.0a11.dist-info/entry_points.txt,sha256=8tV2kynvFkjnhbtDnxAqImL6HMVKsopgGfew0DOp5UY,58
|
245
|
+
fractal_server-2.9.0a11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|