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

Merged
merged 42 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0f27fae
prefix admin
pcrespov Dec 13, 2024
4ac88dc
oas
pcrespov Dec 13, 2024
3bc179f
adds get user
pcrespov Dec 13, 2024
386c8ee
admin and users
pcrespov Dec 13, 2024
0b660ef
drafter interface
pcrespov Dec 13, 2024
aad94f6
acceptance tests
pcrespov Dec 13, 2024
8039691
tests
pcrespov Dec 13, 2024
9374652
drafts
pcrespov Dec 13, 2024
48fe6a0
acceptance test passes
pcrespov Dec 13, 2024
de8ccb5
updates OAS
pcrespov Dec 16, 2024
05ece30
cleanup
pcrespov Dec 16, 2024
24fcf60
minor
pcrespov Dec 17, 2024
3bbe91e
cleanup
pcrespov Dec 17, 2024
923d8f6
drafted groups
pcrespov Dec 17, 2024
57b30eb
drafted tests
pcrespov Dec 17, 2024
c731d42
acceptance test passes
pcrespov Dec 17, 2024
4d9bf75
update OAS
pcrespov Dec 17, 2024
12c1b23
updates search on names
pcrespov Dec 17, 2024
b601b04
updates OAS
pcrespov Dec 17, 2024
6aff278
fixes OAS
pcrespov Dec 17, 2024
2e0d1c3
fixes
pcrespov Dec 17, 2024
58f15e8
adds restricted access
pcrespov Dec 17, 2024
301967a
updates doc
pcrespov Dec 17, 2024
8c69cbc
reverts defaults
pcrespov Dec 17, 2024
b7f2df4
fixes tess
pcrespov Dec 17, 2024
d089ed0
fixes get users in group
pcrespov Dec 17, 2024
b538754
mypy and oas
pcrespov Dec 17, 2024
263a54a
cleanup
pcrespov Dec 17, 2024
103d136
fixes tests
pcrespov Dec 17, 2024
01293f1
@odeimaiz review: deprecated admin users
pcrespov Dec 17, 2024
46239eb
new tests
pcrespov Dec 17, 2024
e170355
common
pcrespov Dec 17, 2024
066c224
pylint
pcrespov Dec 17, 2024
a001f16
cleanup
pcrespov Dec 17, 2024
224764c
reactivated test
pcrespov Dec 18, 2024
f6ab2aa
fixes migration
pcrespov Dec 18, 2024
66f18bd
minor
pcrespov Dec 18, 2024
7d128c5
disables meta test
pcrespov Dec 18, 2024
696e2fc
user_id
pcrespov Dec 18, 2024
733ecdd
reduced duplication
pcrespov Dec 18, 2024
19b0842
reduced duplication
pcrespov Jan 3, 2025
9b9650d
updates migration
pcrespov Jan 3, 2025
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