Skip to content

users

Definition of /auth/users/ routes

list_users(user=Depends(current_active_superuser), db=Depends(get_async_db)) async

Return list of all users

Source code in fractal_server/app/routes/auth/users.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
@router_users.get("/users/", response_model=list[UserRead])
async def list_users(
    user: UserOAuth = Depends(current_active_superuser),
    db: AsyncSession = Depends(get_async_db),
):
    """
    Return list of all users
    """
    stm = select(UserOAuth)
    res = await db.execute(stm)
    user_list = res.scalars().unique().all()

    # Get all user/group links
    stm_all_links = select(LinkUserGroup)
    res = await db.execute(stm_all_links)
    links = res.scalars().all()

    # TODO: possible optimizations for this construction are listed in
    # https://github.com/fractal-analytics-platform/fractal-server/issues/1742
    for ind, user in enumerate(user_list):
        user_list[ind] = dict(
            **user.model_dump(),
            oauth_accounts=user.oauth_accounts,
            group_ids=[
                link.group_id for link in links if link.user_id == user.id
            ],
        )

    return user_list

patch_user(user_id, user_update, current_superuser=Depends(current_active_superuser), user_manager=Depends(get_user_manager), db=Depends(get_async_db)) async

Custom version of the PATCH-user route from fastapi-users.

In order to keep the fastapi-users logic in place (which is convenient to update user attributes), we split the endpoint into two branches. We either go through the fastapi-users-based attribute-update branch, or through the branch where we establish new user/group relationships.

Note that we prevent making both changes at the same time, since it would be more complex to guarantee that endpoint error would leave the database in the same state as before the API call.

Source code in fractal_server/app/routes/auth/users.py
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@router_users.patch("/users/{user_id}/", response_model=UserRead)
async def patch_user(
    user_id: int,
    user_update: UserUpdateWithNewGroupIds,
    current_superuser: UserOAuth = Depends(current_active_superuser),
    user_manager: UserManager = Depends(get_user_manager),
    db: AsyncSession = Depends(get_async_db),
):
    """
    Custom version of the PATCH-user route from `fastapi-users`.

    In order to keep the fastapi-users logic in place (which is convenient to
    update user attributes), we split the endpoint into two branches. We either
    go through the fastapi-users-based attribute-update branch, or through the
    branch where we establish new user/group relationships.

    Note that we prevent making both changes at the same time, since it would
    be more complex to guarantee that endpoint error would leave the database
    in the same state as before the API call.
    """

    # We prevent simultaneous editing of both user attributes and user/group
    # associations
    user_update_dict_without_groups = user_update.dict(
        exclude_unset=True, exclude={"new_group_ids"}
    )
    edit_attributes = user_update_dict_without_groups != {}
    edit_groups = user_update.new_group_ids is not None
    if edit_attributes and edit_groups:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail=(
                "Cannot modify both user attributes and group membership. "
                "Please make two independent PATCH calls"
            ),
        )

    # Check that user exists
    user_to_patch = await _user_or_404(user_id, db)

    if edit_groups:
        # Establish new user/group relationships

        # Check that all required groups exist
        # Note: The reason for introducing `col` is as in
        # https://sqlmodel.tiangolo.com/tutorial/where/#type-annotations-and-errors,
        stm = select(func.count()).where(
            col(UserGroup.id).in_(user_update.new_group_ids)
        )
        res = await db.execute(stm)
        number_matching_groups = res.scalar()
        if number_matching_groups != len(user_update.new_group_ids):
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=(
                    "Not all requested groups (IDs: "
                    f"{user_update.new_group_ids}) exist."
                ),
            )

        for new_group_id in user_update.new_group_ids:
            link = LinkUserGroup(user_id=user_id, group_id=new_group_id)
            db.add(link)

        try:
            await db.commit()
        except IntegrityError as e:
            error_msg = (
                f"Cannot link groups with IDs {user_update.new_group_ids} "
                f"to user {user_id}. "
                "Likely reason: one of these links already exists.\n"
                f"Original error: {str(e)}"
            )
            logger.info(error_msg)
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=error_msg,
            )
        patched_user = user_to_patch
    elif edit_attributes:
        # Modify user attributes
        try:
            user_update_without_groups = UserUpdate(
                **user_update_dict_without_groups
            )
            user = await user_manager.update(
                user_update_without_groups,
                user_to_patch,
                safe=False,
                request=None,
            )
            validated_user = schemas.model_validate(UserOAuth, user)
            patched_user = await db.get(
                UserOAuth, validated_user.id, populate_existing=True
            )
        except exceptions.InvalidPasswordException as e:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail={
                    "code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
                    "reason": e.reason,
                },
            )
        except exceptions.UserAlreadyExists:
            raise HTTPException(
                status.HTTP_400_BAD_REQUEST,
                detail=ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS,
            )
    else:
        # Nothing to do, just continue
        patched_user = user_to_patch

    # Enrich user object with `group_ids` attribute
    patched_user_with_group_ids = await _get_single_user_with_group_ids(
        patched_user, db
    )

    return patched_user_with_group_ids