Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Get and search users applying privacy settings 🗃️ #6966

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
88c2dbc
prefix admin
pcrespov Dec 13, 2024
bbae2be
oas
pcrespov Dec 13, 2024
d43dfd3
adds get user
pcrespov Dec 13, 2024
21ccbe0
admin and users
pcrespov Dec 13, 2024
e99c334
drafter interface
pcrespov Dec 13, 2024
e33efeb
acceptance tests
pcrespov Dec 13, 2024
72751d0
tests
pcrespov Dec 13, 2024
fd9d320
drafts
pcrespov Dec 13, 2024
d5f2dd8
acceptance test passes
pcrespov Dec 13, 2024
f066895
updates OAS
pcrespov Dec 16, 2024
2a13f56
cleanup
pcrespov Dec 16, 2024
ed53e2a
minor
pcrespov Dec 17, 2024
91acb79
cleanup
pcrespov Dec 17, 2024
96d7f4e
drafted groups
pcrespov Dec 17, 2024
2cd0193
drafted tests
pcrespov Dec 17, 2024
fa125d7
acceptance test passes
pcrespov Dec 17, 2024
a76d9a1
update OAS
pcrespov Dec 17, 2024
4c6748d
updates search on names
pcrespov Dec 17, 2024
db860a7
updates OAS
pcrespov Dec 17, 2024
062e508
fixes OAS
pcrespov Dec 17, 2024
f22bd07
fixes
pcrespov Dec 17, 2024
649e6cf
adds restricted access
pcrespov Dec 17, 2024
4e8b6a5
updates doc
pcrespov Dec 17, 2024
07506bb
reverts defaults
pcrespov Dec 17, 2024
620c954
fixes tess
pcrespov Dec 17, 2024
195040a
fixes get users in group
pcrespov Dec 17, 2024
7af1bfa
mypy and oas
pcrespov Dec 17, 2024
ef92fe9
cleanup
pcrespov Dec 17, 2024
1394397
fixes tests
pcrespov Dec 17, 2024
8d088c1
@odeimaiz review: deprecated admin users
pcrespov Dec 17, 2024
1834e83
new tests
pcrespov Dec 17, 2024
72954f4
common
pcrespov Dec 17, 2024
694a0d7
pylint
pcrespov Dec 17, 2024
4aaa20d
cleanup
pcrespov Dec 17, 2024
d0bfb17
reactivated test
pcrespov Dec 18, 2024
1d39673
fixes migration
pcrespov Dec 18, 2024
e937280
minor
pcrespov Dec 18, 2024
490a5c3
disables meta test
pcrespov Dec 18, 2024
82a5348
user_id
pcrespov Dec 18, 2024
c94a135
reduced duplication
pcrespov Dec 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
pcrespov marked this conversation as resolved.
Show resolved Hide resolved
)
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
Loading