Skip to content

Commit 706b034

Browse files
pcrespovmrnicegyu11
authored andcommitted
✨ web-api: User privacy policy extended to username 🗃️ (ITISFoundation#7402)
1 parent 23a1ecf commit 706b034

File tree

17 files changed

+289
-133
lines changed

17 files changed

+289
-133
lines changed

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

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -244,33 +244,31 @@ def from_domain_model(
244244
GroupGet.from_domain_model(*gi) for gi in groups_by_type.standard
245245
],
246246
all=GroupGet.from_domain_model(*groups_by_type.everyone),
247-
product=GroupGet.from_domain_model(*my_product_group)
248-
if my_product_group
249-
else None,
247+
product=(
248+
GroupGet.from_domain_model(*my_product_group)
249+
if my_product_group
250+
else None
251+
),
250252
)
251253

252254

253255
class GroupUserGet(OutputSchemaWithoutCamelCase):
254256

255-
# Identifiers
256257
id: Annotated[UserID | None, Field(description="the user's id")] = None
257-
user_name: Annotated[UserNameID, Field(alias="userName")]
258+
user_name: Annotated[
259+
UserNameID | None, Field(alias="userName", description="None if private")
260+
] = None
258261
gid: Annotated[
259262
GroupID | None,
260263
Field(description="the user primary gid"),
261264
] = None
262265

263-
# Private Profile
264266
login: Annotated[
265267
LowerCaseEmailStr | None,
266-
Field(description="the user's email, if privacy settings allows"),
267-
] = None
268-
first_name: Annotated[
269-
str | None, Field(description="If privacy settings allows")
270-
] = None
271-
last_name: Annotated[
272-
str | None, Field(description="If privacy settings allows")
268+
Field(description="the user's email or None if private"),
273269
] = None
270+
first_name: Annotated[str | None, Field(description="None if private")] = None
271+
last_name: Annotated[str | None, Field(description="None if private")] = None
274272
gravatar_id: Annotated[
275273
str | None, Field(description="the user gravatar id hash", deprecated=True)
276274
] = None
@@ -309,6 +307,11 @@ class GroupUserGet(OutputSchemaWithoutCamelCase):
309307
"userName": "mrprivate",
310308
"gid": "55",
311309
},
310+
# very private user
311+
{
312+
"id": "6",
313+
"gid": "55",
314+
},
312315
{
313316
"id": "56",
314317
"userName": "mrpublic",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ class UserGet(OutputSchema):
215215
# Public profile of a user subject to its privacy settings
216216
user_id: UserID
217217
group_id: GroupID
218-
user_name: UserNameID
218+
user_name: UserNameID | None = None
219219
first_name: str | None = None
220220
last_name: str | None = None
221221
email: EmailStr | None = None

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
TypedDict,
1010
)
1111

12-
from .basic_types import IDStr
13-
from .users import UserID
12+
from .users import UserID, UserNameID
1413
from .utils.common_validators import create_enums_pre_validator
1514

1615
EVERYONE_GROUP_ID: Final[int] = 1
@@ -99,10 +98,10 @@ class GroupsByTypeTuple(NamedTuple):
9998
class GroupMember(BaseModel):
10099
# identifiers
101100
id: UserID
102-
name: IDStr
103101
primary_gid: GroupID
104102

105103
# private profile
104+
name: UserNameID | None
106105
email: EmailStr | None
107106
first_name: str | None
108107
last_name: str | None

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525

2626

2727
class PrivacyDict(TypedDict):
28+
hide_username: bool
2829
hide_fullname: bool
2930
hide_email: bool
3031

3132

3233
class MyProfile(BaseModel):
3334
id: UserID
34-
user_name: IDStr
35+
user_name: UserNameID
3536
first_name: str | None
3637
last_name: str | None
3738
email: LowerCaseEmailStr
@@ -50,7 +51,11 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
5051
"first_name": "PtN5Ab0uv",
5152
"last_name": "",
5253
"role": "GUEST",
53-
"privacy": {"hide_email": True, "hide_fullname": False},
54+
"privacy": {
55+
"hide_email": True,
56+
"hide_fullname": False,
57+
"hide_username": False,
58+
},
5459
}
5560
}
5661
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""new users.privacy_hide_username column
2+
3+
Revision ID: 8403acca8759
4+
Revises: f7f3c835f38a
5+
Create Date: 2025-03-20 14:08:48.321587+00:00
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "8403acca8759"
14+
down_revision = "f7f3c835f38a"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column(
22+
"users",
23+
sa.Column(
24+
"privacy_hide_username",
25+
sa.Boolean(),
26+
server_default=sa.text("false"),
27+
nullable=False,
28+
),
29+
)
30+
# ### end Alembic commands ###
31+
32+
33+
def downgrade():
34+
# ### commands auto generated by Alembic - please adjust! ###
35+
op.drop_column("users", "privacy_hide_username")
36+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/users.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@
9595
#
9696
# User Privacy Rules ------------------
9797
#
98+
sa.Column(
99+
"privacy_hide_username",
100+
sa.Boolean,
101+
nullable=False,
102+
server_default=expression.false(),
103+
doc="If true, it hides users.name to others",
104+
),
98105
sa.Column(
99106
"privacy_hide_fullname",
100107
sa.Boolean,

packages/postgres-database/src/simcore_postgres_database/utils_users.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,16 @@ def is_public(hide_attribute: Column, caller_id: int):
242242
return hide_attribute.is_(False) | (users.c.id == caller_id)
243243

244244

245-
def visible_user_profile_cols(caller_id: int):
245+
def visible_user_profile_cols(caller_id: int, *, username_label: str):
246246
"""Returns user profile columns with visibility constraints applied based on privacy settings."""
247247
return (
248+
sa.case(
249+
(
250+
is_private(users.c.privacy_hide_username, caller_id),
251+
None,
252+
),
253+
else_=users.c.name,
254+
).label(username_label),
248255
sa.case(
249256
(
250257
is_private(users.c.privacy_hide_email, caller_id),

packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import contextlib
12
import re
3+
from collections.abc import AsyncIterator
24
from datetime import datetime
35
from typing import Any, TypedDict
46

@@ -186,6 +188,30 @@ async def __aexit__(self, *args):
186188
return await super().__aexit__(*args)
187189

188190

191+
@contextlib.asynccontextmanager
192+
async def switch_client_session_to(
193+
client: TestClient, user: UserInfoDict
194+
) -> AsyncIterator[TestClient]:
195+
assert client.app
196+
197+
await client.post(f'{client.app.router["auth_logout"].url_for()}')
198+
# sometimes 4xx if user already logged out. Ignore
199+
200+
resp = await client.post(
201+
f'{client.app.router["auth_login"].url_for()}',
202+
json={
203+
"email": user["email"],
204+
"password": user["raw_password"],
205+
},
206+
)
207+
await assert_status(resp, status.HTTP_200_OK)
208+
209+
yield client
210+
211+
resp = await client.post(f'{client.app.router["auth_logout"].url_for()}')
212+
await assert_status(resp, status.HTTP_200_OK)
213+
214+
189215
class NewInvitation(NewUser):
190216
def __init__(
191217
self,

services/web/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.61.2
1+
0.61.3

services/web/server/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.61.2
2+
current_version = 0.61.3
33
commit = True
44
message = services/webserver api version: {current_version} → {new_version}
55
tag = False

0 commit comments

Comments
 (0)