Skip to content

Commit

Permalink
✨ Get and search users applying privacy settings 🗃️ (#6966)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored Jan 3, 2025
1 parent 493488c commit 6f0c82c
Show file tree
Hide file tree
Showing 24 changed files with 886 additions and 238 deletions.
2 changes: 1 addition & 1 deletion api/specs/web-server/_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
response_model=Envelope[Union[EmailTestFailed, EmailTestPassed]],
)
async def test_email(
_test: TestEmail, x_simcore_products_name: str | None = Header(default=None)
_body: TestEmail, x_simcore_products_name: str | None = Header(default=None)
):
# X-Simcore-Products-Name
...
11 changes: 10 additions & 1 deletion api/specs/web-server/_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# pylint: disable=too-many-arguments


from enum import Enum
from typing import Annotated, Any

from fastapi import APIRouter, Depends, status
Expand Down Expand Up @@ -87,19 +88,24 @@ async def delete_group(_path: Annotated[GroupsPathParams, Depends()]):
"""


_extra_tags: list[str | Enum] = ["users"]


@router.get(
"/groups/{gid}/users",
response_model=Envelope[list[GroupUserGet]],
tags=_extra_tags,
)
async def get_all_group_users(_path: Annotated[GroupsPathParams, Depends()]):
"""
Gets users in organization groups
Gets users in organization or primary groups
"""


@router.post(
"/groups/{gid}/users",
status_code=status.HTTP_204_NO_CONTENT,
tags=_extra_tags,
)
async def add_group_user(
_path: Annotated[GroupsPathParams, Depends()],
Expand All @@ -113,6 +119,7 @@ async def add_group_user(
@router.get(
"/groups/{gid}/users/{uid}",
response_model=Envelope[GroupUserGet],
tags=_extra_tags,
)
async def get_group_user(
_path: Annotated[GroupsUsersPathParams, Depends()],
Expand All @@ -125,6 +132,7 @@ async def get_group_user(
@router.patch(
"/groups/{gid}/users/{uid}",
response_model=Envelope[GroupUserGet],
tags=_extra_tags,
)
async def update_group_user(
_path: Annotated[GroupsUsersPathParams, Depends()],
Expand All @@ -138,6 +146,7 @@ async def update_group_user(
@router.delete(
"/groups/{gid}/users/{uid}",
status_code=status.HTTP_204_NO_CONTENT,
tags=_extra_tags,
)
async def delete_group_user(
_path: Annotated[GroupsUsersPathParams, Depends()],
Expand Down
70 changes: 48 additions & 22 deletions api/specs/web-server/_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# pylint: disable=too-many-arguments


from enum import Enum
from typing import Annotated

from fastapi import APIRouter, Depends, status
Expand All @@ -13,8 +14,10 @@
MyProfilePatch,
MyTokenCreate,
MyTokenGet,
UserForAdminGet,
UserGet,
UsersSearchQueryParams,
UsersForAdminSearchQueryParams,
UsersSearch,
)
from models_library.api_schemas_webserver.users_preferences import PatchRequestBody
from models_library.generics import Envelope
Expand All @@ -29,7 +32,7 @@
from simcore_service_webserver.users._notifications_rest import _NotificationPathParams
from simcore_service_webserver.users._tokens_rest import _TokenPathParams

router = APIRouter(prefix=f"/{API_VTAG}", tags=["user"])
router = APIRouter(prefix=f"/{API_VTAG}", tags=["users"])


@router.get(
Expand All @@ -44,7 +47,7 @@ async def get_my_profile():
"/me",
status_code=status.HTTP_204_NO_CONTENT,
)
async def update_my_profile(_profile: MyProfilePatch):
async def update_my_profile(_body: MyProfilePatch):
...


Expand All @@ -54,7 +57,7 @@ async def update_my_profile(_profile: MyProfilePatch):
deprecated=True,
description="Use PATCH instead",
)
async def replace_my_profile(_profile: MyProfilePatch):
async def replace_my_profile(_body: MyProfilePatch):
...


Expand All @@ -64,7 +67,7 @@ async def replace_my_profile(_profile: MyProfilePatch):
)
async def set_frontend_preference(
preference_id: PreferenceIdentifier,
body_item: PatchRequestBody,
_body: PatchRequestBody,
):
...

Expand All @@ -82,23 +85,25 @@ async def list_tokens():
response_model=Envelope[MyTokenGet],
status_code=status.HTTP_201_CREATED,
)
async def create_token(_token: MyTokenCreate):
async def create_token(_body: MyTokenCreate):
...


@router.get(
"/me/tokens/{service}",
response_model=Envelope[MyTokenGet],
)
async def get_token(_params: Annotated[_TokenPathParams, Depends()]):
async def get_token(
_path: Annotated[_TokenPathParams, Depends()],
):
...


@router.delete(
"/me/tokens/{service}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_token(_params: Annotated[_TokenPathParams, Depends()]):
async def delete_token(_path: Annotated[_TokenPathParams, Depends()]):
...


Expand All @@ -114,7 +119,9 @@ async def list_user_notifications():
"/me/notifications",
status_code=status.HTTP_204_NO_CONTENT,
)
async def create_user_notification(_notification: UserNotificationCreate):
async def create_user_notification(
_body: UserNotificationCreate,
):
...


Expand All @@ -123,8 +130,8 @@ async def create_user_notification(_notification: UserNotificationCreate):
status_code=status.HTTP_204_NO_CONTENT,
)
async def mark_notification_as_read(
_params: Annotated[_NotificationPathParams, Depends()],
_notification: UserNotificationPatch,
_path: Annotated[_NotificationPathParams, Depends()],
_body: UserNotificationPatch,
):
...

Expand All @@ -137,24 +144,43 @@ async def list_user_permissions():
...


@router.get(
#
# USERS public
#


@router.post(
"/users:search",
response_model=Envelope[list[UserGet]],
tags=[
"po",
],
description="Search among users who are publicly visible to the caller (i.e., me) based on their privacy settings.",
)
async def search_users(_params: Annotated[UsersSearchQueryParams, Depends()]):
async def search_users(_body: UsersSearch):
...


#
# USERS admin
#

_extra_tags: list[str | Enum] = ["admin"]


@router.get(
"/admin/users:search",
response_model=Envelope[list[UserForAdminGet]],
tags=_extra_tags,
)
async def search_users_for_admin(
_query: Annotated[UsersForAdminSearchQueryParams, Depends()]
):
# NOTE: see `Search` in `Common Custom Methods` in https://cloud.google.com/apis/design/custom_methods
...


@router.post(
"/users:pre-register",
response_model=Envelope[UserGet],
tags=[
"po",
],
"/admin/users:pre-register",
response_model=Envelope[UserForAdminGet],
tags=_extra_tags,
)
async def pre_register_user(_body: PreRegisteredUserGet):
async def pre_register_user_for_admin(_body: PreRegisteredUserGet):
...
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
)
from ..users import UserID, UserNameID
from ..utils.common_validators import create__check_only_one_is_set__root_validator
from ._base import InputSchema, OutputSchema
from ._base import InputSchema, OutputSchema, OutputSchemaWithoutCamelCase

S = TypeVar("S", bound=BaseModel)

Expand Down Expand Up @@ -248,8 +248,7 @@ def from_model(
)


class GroupUserGet(BaseModel):
# OutputSchema
class GroupUserGet(OutputSchemaWithoutCamelCase):

# Identifiers
id: Annotated[UserID | None, Field(description="the user's id")] = None
Expand All @@ -275,7 +274,14 @@ class GroupUserGet(BaseModel):
] = None

# Access Rights
access_rights: GroupAccessRights = Field(..., alias="accessRights")
access_rights: Annotated[
GroupAccessRights | None,
Field(
alias="accessRights",
description="If group is standard, these are these are the access rights of the user to it."
"None if primary group.",
),
] = None

model_config = ConfigDict(
populate_by_name=True,
Expand All @@ -293,7 +299,23 @@ class GroupUserGet(BaseModel):
"write": False,
"delete": False,
},
}
},
"examples": [
# unique member on a primary group with two different primacy settings
{
"id": "16",
"userName": "mrprivate",
"gid": "55",
},
{
"id": "56",
"userName": "mrpublic",
"login": "[email protected]",
"first_name": "Mr",
"last_name": "Public",
"gid": "42",
},
],
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,31 @@
from enum import Enum
from typing import Annotated, Any, Literal, Self

import annotated_types
from common_library.basic_types import DEFAULT_FACTORY
from common_library.dict_tools import remap_keys
from common_library.users_enums import UserStatus
from models_library.groups import AccessRightsDict
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
from pydantic import (
ConfigDict,
EmailStr,
Field,
StringConstraints,
ValidationInfo,
field_validator,
)

from ..basic_types import IDStr
from ..emails import LowerCaseEmailStr
from ..groups import AccessRightsDict, Group, GroupsByTypeTuple
from ..groups import AccessRightsDict, Group, GroupID, GroupsByTypeTuple
from ..products import ProductName
from ..rest_base import RequestParameters
from ..users import (
FirstNameStr,
LastNameStr,
MyProfile,
UserID,
UserNameID,
UserPermission,
UserThirdPartyToken,
)
Expand Down Expand Up @@ -185,7 +195,37 @@ def _validate_user_name(cls, value: str):
#


class UsersSearchQueryParams(BaseModel):
class UsersGetParams(RequestParameters):
user_id: UserID


class UsersSearch(InputSchema):
match_: Annotated[
str,
StringConstraints(strip_whitespace=True, min_length=1, max_length=80),
Field(
description="Search string to match with usernames and public profiles (e.g. emails, first/last name)",
alias="match",
),
]
limit: Annotated[int, annotated_types.Interval(ge=1, le=50)] = 10


class UserGet(OutputSchema):
# Public profile of a user subject to its privacy settings
user_id: UserID
group_id: GroupID
user_name: UserNameID
first_name: str | None = None
last_name: str | None = None
email: EmailStr | None = None

@classmethod
def from_model(cls, data):
return cls.model_validate(data, from_attributes=True)


class UsersForAdminSearchQueryParams(RequestParameters):
email: Annotated[
str,
Field(
Expand All @@ -196,7 +236,8 @@ class UsersSearchQueryParams(BaseModel):
]


class UserGet(OutputSchema):
class UserForAdminGet(OutputSchema):
# ONLY for admins
first_name: str | None
last_name: str | None
email: LowerCaseEmailStr
Expand Down
2 changes: 1 addition & 1 deletion packages/models-library/src/models_library/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class GroupMember(BaseModel):
last_name: str | None

# group access
access_rights: AccessRightsDict
access_rights: AccessRightsDict | None = None

model_config = ConfigDict(from_attributes=True)

Expand Down
Loading

0 comments on commit 6f0c82c

Please sign in to comment.