Skip to content

Commit 3f7c322

Browse files
authored
✨ Support Center: Gives Read Access to Support Users to User Accounts and Product Details (#8313)
1 parent bbb7767 commit 3f7c322

File tree

28 files changed

+721
-184
lines changed

28 files changed

+721
-184
lines changed

packages/models-library/src/models_library/api_schemas_webserver/users.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22
from datetime import date, datetime
33
from enum import Enum
4-
from typing import Annotated, Any, Literal, Self
4+
from typing import Annotated, Any, Literal, Self, TypeAlias
55

66
import annotated_types
77
from common_library.basic_types import DEFAULT_FACTORY
@@ -18,6 +18,7 @@
1818
StringConstraints,
1919
ValidationInfo,
2020
field_validator,
21+
model_validator,
2122
)
2223
from pydantic.config import JsonDict
2324

@@ -83,7 +84,14 @@ class MyProfileRestGet(OutputSchemaWithoutCamelCase):
8384
login: LowerCaseEmailStr
8485
phone: str | None = None
8586

86-
role: Literal["ANONYMOUS", "GUEST", "USER", "TESTER", "PRODUCT_OWNER", "ADMIN"]
87+
role: Literal[
88+
"ANONYMOUS",
89+
"GUEST",
90+
"USER",
91+
"TESTER",
92+
"PRODUCT_OWNER",
93+
"ADMIN",
94+
]
8795
groups: MyGroupsGet | None = None
8896
gravatar_id: Annotated[str | None, Field(deprecated=True)] = None
8997

@@ -306,15 +314,41 @@ class UserAccountReject(InputSchema):
306314
email: EmailStr
307315

308316

317+
GlobString: TypeAlias = Annotated[
318+
str,
319+
StringConstraints(
320+
min_length=3, max_length=200, strip_whitespace=True, pattern=r"^[^%]*$"
321+
),
322+
]
323+
324+
309325
class UserAccountSearchQueryParams(RequestParameters):
310326
email: Annotated[
311-
str,
327+
GlobString | None,
312328
Field(
313-
min_length=3,
314-
max_length=200,
315329
description="complete or glob pattern for an email",
316330
),
317-
]
331+
] = None
332+
primary_group_id: Annotated[
333+
GroupID | None,
334+
Field(
335+
description="Filter by primary group ID",
336+
),
337+
] = None
338+
user_name: Annotated[
339+
GlobString | None,
340+
Field(
341+
description="complete or glob pattern for a username",
342+
),
343+
] = None
344+
345+
@model_validator(mode="after")
346+
def _validate_at_least_one_filter(self) -> Self:
347+
field_names = list(self.__class__.model_fields)
348+
if not any(getattr(self, field_name, None) for field_name in field_names):
349+
msg = f"At least one filter {field_names} must be provided"
350+
raise ValueError(msg)
351+
return self
318352

319353

320354
class UserAccountGet(OutputSchema):
@@ -340,9 +374,9 @@ class UserAccountGet(OutputSchema):
340374
# pre-registration NOTE: that some users have no pre-registartion and therefore all options here can be none
341375
pre_registration_id: int | None
342376
pre_registration_created: datetime | None
343-
invited_by: str | None = None
377+
invited_by: UserNameID | None = None
344378
account_request_status: AccountRequestStatus | None
345-
account_request_reviewed_by: UserID | None = None
379+
account_request_reviewed_by: UserNameID | None = None
346380
account_request_reviewed_at: datetime | None = None
347381

348382
# user status

packages/models-library/src/models_library/users.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import Annotated, TypeAlias
33

44
from common_library.users_enums import UserRole
5-
from models_library.basic_types import IDStr
65
from pydantic import BaseModel, ConfigDict, Field, PositiveInt, StringConstraints
76
from pydantic.config import JsonDict
87
from typing_extensions import ( # https://docs.pydantic.dev/latest/api/standard_library_types/#typeddict
@@ -12,8 +11,9 @@
1211
from .emails import LowerCaseEmailStr
1312

1413
UserID: TypeAlias = PositiveInt
15-
UserNameID: TypeAlias = IDStr
16-
14+
UserNameID: TypeAlias = Annotated[
15+
str, StringConstraints(strip_whitespace=True, min_length=1, max_length=100)
16+
]
1717

1818
FirstNameStr: TypeAlias = Annotated[
1919
str, StringConstraints(strip_whitespace=True, max_length=255)

services/api-server/setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ commit_args = --no-verify
1111
asyncio_mode = auto
1212
asyncio_default_fixture_loop_scope = function
1313
addopts = --strict-markers
14-
markers =
14+
markers =
1515
slow: marks tests as slow (deselect with '-m "not slow"')
1616
acceptance_test: "marks tests as 'acceptance tests' i.e. does the system do what the user expects? Typically those are workflows."
1717
testit: "marks test to run during development"
1818

1919
[mypy]
20-
plugins =
20+
plugins =
2121
pydantic.mypy
2222
sqlalchemy.ext.mypy.plugin

services/api-server/src/simcore_service_api_server/models/schemas/profiles.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ class ProfileCommon(BaseModel):
1313
last_name: LastNameStr | None = Field(None, examples=["Maxwell"])
1414

1515

16-
class ProfileUpdate(ProfileCommon):
17-
...
16+
class ProfileUpdate(ProfileCommon): ...
1817

1918

2019
class UserRoleEnum(StrAutoEnum):

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.77.0
1+
0.77.2

services/web/server/setup.cfg

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.77.0
2+
current_version = 0.77.2
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False
@@ -13,13 +13,13 @@ commit_args = --no-verify
1313
addopts = --strict-markers
1414
asyncio_mode = auto
1515
asyncio_default_fixture_loop_scope = function
16-
markers =
16+
markers =
1717
slow: marks tests as slow (deselect with '-m "not slow"')
1818
acceptance_test: "marks tests as 'acceptance tests' i.e. does the system do what the user expects? Typically those are workflows."
1919
testit: "marks test to run during development"
2020
heavy_load: "mark tests that require large amount of data"
2121

2222
[mypy]
23-
plugins =
23+
plugins =
2424
pydantic.mypy
2525
sqlalchemy.ext.mypy.plugin

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.0
22
info:
33
title: simcore-service-webserver
44
description: Main service with an interface (http-API & websockets) to the web front-end
5-
version: 0.77.0
5+
version: 0.77.2
66
servers:
77
- url: ''
88
description: webserver
@@ -1798,12 +1798,36 @@ paths:
17981798
parameters:
17991799
- name: email
18001800
in: query
1801-
required: true
1801+
required: false
18021802
schema:
1803-
type: string
1804-
minLength: 3
1805-
maxLength: 200
1803+
anyOf:
1804+
- type: string
1805+
minLength: 3
1806+
maxLength: 200
1807+
pattern: ^[^%]*$
1808+
- type: 'null'
18061809
title: Email
1810+
- name: primary_group_id
1811+
in: query
1812+
required: false
1813+
schema:
1814+
anyOf:
1815+
- type: integer
1816+
exclusiveMinimum: true
1817+
minimum: 0
1818+
- type: 'null'
1819+
title: Primary Group Id
1820+
- name: user_name
1821+
in: query
1822+
required: false
1823+
schema:
1824+
anyOf:
1825+
- type: string
1826+
minLength: 3
1827+
maxLength: 200
1828+
pattern: ^[^%]*$
1829+
- type: 'null'
1830+
title: User Name
18071831
responses:
18081832
'200':
18091833
description: Successful Response
@@ -18361,6 +18385,8 @@ components:
1836118385
invitedBy:
1836218386
anyOf:
1836318387
- type: string
18388+
maxLength: 100
18389+
minLength: 1
1836418390
- type: 'null'
1836518391
title: Invitedby
1836618392
accountRequestStatus:
@@ -18369,9 +18395,9 @@ components:
1836918395
- type: 'null'
1837018396
accountRequestReviewedBy:
1837118397
anyOf:
18372-
- type: integer
18373-
exclusiveMinimum: true
18374-
minimum: 0
18398+
- type: string
18399+
maxLength: 100
18400+
minLength: 1
1837518401
- type: 'null'
1837618402
title: Accountrequestreviewedby
1837718403
accountRequestReviewedAt:

services/web/server/src/simcore_service_webserver/groups/_groups_repository.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from aiohttp import web
77
from common_library.groups_enums import GroupType
88
from common_library.users_enums import UserRole
9-
from models_library.basic_types import IDStr
109
from models_library.groups import (
1110
AccessRightsDict,
1211
Group,
@@ -17,7 +16,7 @@
1716
StandardGroupCreate,
1817
StandardGroupUpdate,
1918
)
20-
from models_library.users import UserID
19+
from models_library.users import UserID, UserNameID
2120
from simcore_postgres_database.aiopg_errors import UniqueViolation
2221
from simcore_postgres_database.models.users import users
2322
from simcore_postgres_database.utils_products import get_or_create_product_group
@@ -732,14 +731,30 @@ async def is_user_by_email_in_group(
732731
return user_id is not None
733732

734733

734+
async def is_user_in_group(
735+
app: web.Application,
736+
connection: AsyncConnection | None = None,
737+
*,
738+
user_id: UserID,
739+
group_id: GroupID,
740+
) -> bool:
741+
async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn:
742+
result = await conn.scalar(
743+
sa.select(user_to_groups.c.uid).where(
744+
(user_to_groups.c.uid == user_id) & (user_to_groups.c.gid == group_id)
745+
)
746+
)
747+
return result is not None
748+
749+
735750
async def add_new_user_in_group(
736751
app: web.Application,
737752
connection: AsyncConnection | None = None,
738753
*,
739754
group_id: GroupID,
740755
# either user_id or user_name
741756
new_user_id: UserID | None = None,
742-
new_user_name: IDStr | None = None,
757+
new_user_name: UserNameID | None = None,
743758
access_rights: AccessRightsDict | None = None,
744759
) -> None:
745760
"""

services/web/server/src/simcore_service_webserver/groups/_groups_service.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from contextlib import suppress
22

33
from aiohttp import web
4-
from models_library.basic_types import IDStr
54
from models_library.emails import LowerCaseEmailStr
65
from models_library.groups import (
76
AccessRightsDict,
@@ -13,7 +12,7 @@
1312
StandardGroupUpdate,
1413
)
1514
from models_library.products import ProductName
16-
from models_library.users import UserID
15+
from models_library.users import UserID, UserNameID
1716
from pydantic import EmailStr
1817

1918
from ..products.models import Product
@@ -262,6 +261,14 @@ async def is_user_by_email_in_group(
262261
)
263262

264263

264+
async def is_user_in_group(
265+
app: web.Application, *, user_id: UserID, group_id: GroupID
266+
) -> bool:
267+
return await _groups_repository.is_user_in_group(
268+
app, user_id=user_id, group_id=group_id
269+
)
270+
271+
265272
async def auto_add_user_to_groups(app: web.Application, user_id: UserID) -> None:
266273
user: dict = await users_service.get_user(app, user_id)
267274
return await _groups_repository.auto_add_user_to_groups(app, user=user)
@@ -288,7 +295,7 @@ async def add_user_in_group(
288295
*,
289296
# identifies
290297
new_by_user_id: UserID | None = None,
291-
new_by_user_name: IDStr | None = None,
298+
new_by_user_name: UserNameID | None = None,
292299
new_by_user_email: EmailStr | None = None,
293300
access_rights: AccessRightsDict | None = None,
294301
) -> None:

services/web/server/src/simcore_service_webserver/groups/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
get_product_group_for_user,
1010
get_user_profile_groups,
1111
is_user_by_email_in_group,
12+
is_user_in_group,
1213
list_all_user_groups_ids,
1314
list_group_members,
1415
list_user_groups_ids_with_read_access,
@@ -23,6 +24,7 @@
2324
"get_product_group_for_user",
2425
"get_user_profile_groups",
2526
"is_user_by_email_in_group",
27+
"is_user_in_group",
2628
"list_all_user_groups_ids",
2729
"list_group_members",
2830
"list_user_groups_ids_with_read_access",

0 commit comments

Comments
 (0)