Skip to content

Commit b80cd42

Browse files
authored
fix: do not return secondary emails in member listing/details (#83556)
Fixes #55234. We only display the user's primary email address in the UI. There's no reason to send the secondary emails of the user in the API response.
1 parent 6c3b537 commit b80cd42

File tree

4 files changed

+68
-3
lines changed

4 files changed

+68
-3
lines changed

src/sentry/api/serializers/models/organization_member/base.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def get_attrs(
4444
users_by_id: MutableMapping[str, Any] = {}
4545
email_map: MutableMapping[str, str] = {}
4646
for u in user_service.serialize_many(filter={"user_ids": users_set}):
47+
# Filter out the emails from the user data
48+
u.pop("emails", None)
4749
users_by_id[u["id"]] = u
4850
email_map[u["id"]] = u["email"]
4951

@@ -96,17 +98,39 @@ def serialize(
9698
user: User | RpcUser | AnonymousUser,
9799
**kwargs: Any,
98100
) -> OrganizationMemberResponse:
101+
serialized_user = attrs["user"]
102+
if obj.user_id:
103+
# if the OrganizationMember has a user_id, the user has an account
104+
# `email` on the OrganizationMember will be null, so we need to pull
105+
# the email address from the user's actual account
106+
email = serialized_user["email"] if serialized_user else obj.email
107+
else:
108+
# when there is no user_id, the OrganizationMember is an invited user
109+
# and the email field on OrganizationMember will be populated, so we
110+
# will use it directly
111+
email = obj.email
112+
113+
# helping mypy - the email will always be populated at this point
114+
# in the case that it is a user that has an account, we pull the email
115+
# address above from the serialized_user. The email field on OrganizationMember
116+
# is null in the case, so it is necessary.
117+
#
118+
# invited users do not yet have a full account and the email field
119+
# on OrganizationMember will be populated in such cases
120+
assert email is not None
121+
99122
inviter_name = None
100123
if obj.inviter_id:
101124
inviter = attrs["inviter"]
102125
if inviter:
103126
inviter_name = inviter.get_display_name()
104-
serialized_user = attrs["user"]
105-
email = attrs["email"]
127+
128+
name = serialized_user["name"] if serialized_user else email
129+
106130
data: OrganizationMemberResponse = {
107131
"id": str(obj.id),
108132
"email": email,
109-
"name": serialized_user["name"] if serialized_user else email,
133+
"name": name,
110134
"user": attrs["user"],
111135
"orgRole": obj.role,
112136
"pending": obj.is_pending,

src/sentry/api/serializers/models/organization_member/expand/roles.py

+5
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def get_attrs(
5656
serializer=UserSerializeType.DETAILED,
5757
)
5858
}
59+
60+
# Filter out emails from the serialized user data
61+
for user_data in users_by_id.values():
62+
user_data.pop("emails", None)
63+
5964
for item in item_list:
6065
result.setdefault(item, {})["serializedUser"] = users_by_id.get(str(item.user_id), {})
6166
return result

tests/sentry/api/endpoints/test_organization_member_details.py

+16
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,22 @@ def test_lists_team_roles(self):
142142
role_ids = [role["id"] for role in response.data["teamRoleList"]]
143143
assert role_ids == ["contributor", "admin"]
144144

145+
def test_does_not_include_secondary_emails(self):
146+
# Create a user with multiple email addresses
147+
user = self.create_user("[email protected]", username="multi_email_user")
148+
self.create_useremail(user, "[email protected]")
149+
self.create_useremail(user, "[email protected]")
150+
151+
# Add user to organization
152+
member = self.create_member(organization=self.organization, user=user, role="member")
153+
154+
response = self.get_success_response(self.organization.slug, member.id)
155+
156+
# Check that only primary email is present and no other email addresses are exposed
157+
assert response.data["email"] == "[email protected]"
158+
assert "emails" not in response.data["user"]
159+
assert "emails" not in response.data.get("serializedUser", {})
160+
145161

146162
class UpdateOrganizationMemberTest(OrganizationMemberTestBase, HybridCloudTestMixin):
147163
method = "put"

tests/sentry/api/endpoints/test_organization_member_index.py

+20
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,26 @@ def test_cannot_invite_retired_role_with_flag(self):
517517
== "The role 'admin' is deprecated and may no longer be assigned."
518518
)
519519

520+
def test_does_not_include_secondary_emails(self):
521+
# Create a user with multiple email addresses
522+
user3 = self.create_user("[email protected]", username="multi_email_user")
523+
self.create_useremail(user3, "[email protected]")
524+
self.create_useremail(user3, "[email protected]")
525+
526+
# Add user to organization
527+
self.create_member(organization=self.organization, user=user3)
528+
529+
response = self.get_success_response(self.organization.slug)
530+
531+
# Find the member in the response
532+
member_data = next(m for m in response.data if m["email"] == "[email protected]")
533+
534+
# Check that only primary email is present and no other email addresses are exposed
535+
assert member_data["email"] == "[email protected]"
536+
assert "emails" not in member_data["user"]
537+
assert all("email" not in team for team in member_data.get("teams", []))
538+
assert all("email" not in role for role in member_data.get("teamRoles", []))
539+
520540

521541
class OrganizationMemberPermissionRoleTest(OrganizationMemberListTestBase, HybridCloudTestMixin):
522542
method = "post"

0 commit comments

Comments
 (0)