Skip to content

Commit 71e345a

Browse files
committed
Fix bug with private profiles
1 parent ebcf075 commit 71e345a

10 files changed

Lines changed: 459 additions & 40 deletions

astra_app/core/email_context.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,46 @@ def user_email_context_from_user(*, user: FreeIPAUser) -> dict[str, str]:
7272
}
7373

7474

75+
def user_email_delivery_context(*, username: str) -> dict[str, str]:
76+
"""Return user email context for operational delivery paths.
77+
78+
Privacy settings control profile visibility, but delivery pathways must
79+
always resolve the real address when available.
80+
"""
81+
82+
normalized_username = str(username or "").strip()
83+
if not normalized_username:
84+
return {"username": "", "first_name": "", "last_name": "", "full_name": "", "email": ""}
85+
86+
try:
87+
user = FreeIPAUser.get(normalized_username, respect_privacy=False)
88+
except Exception as exc:
89+
reason_code = str(exc.__class__.__name__ or "").strip() or "freeipa_unavailable"
90+
logger.warning(
91+
"Delivery email context degraded due to FreeIPA lookup failure",
92+
extra={
93+
"event": "astra.email.delivery_context.degraded",
94+
"component": "email",
95+
"outcome": "warning",
96+
"reason_code": reason_code,
97+
"freeipa_operation": "FreeIPAUser.get",
98+
"correlation_id": "email_context.user_email_delivery_context",
99+
},
100+
)
101+
user = None
102+
103+
if user is None:
104+
return {
105+
"username": normalized_username,
106+
"first_name": "",
107+
"last_name": "",
108+
"full_name": normalized_username,
109+
"email": "",
110+
}
111+
112+
return user_email_context_from_user(user=user)
113+
114+
75115
def organization_email_context_from_organization(*, organization: Organization) -> dict[str, str]:
76116
"""Return canonical organization variables for sponsor-facing templated emails.
77117

astra_app/core/freeipa/user.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def _try(label: str, fn):
349349
return None
350350

351351
@classmethod
352-
def get(cls, username):
352+
def get(cls, username, *, respect_privacy: bool = True):
353353
"""
354354
Fetch a single user by username.
355355
"""
@@ -359,7 +359,7 @@ def get(cls, username):
359359
cached_data = cache.get(cache_key)
360360

361361
if cached_data is not None:
362-
return cls(username, cached_data)
362+
return cls(username, cached_data, respect_privacy=respect_privacy)
363363

364364
try:
365365
user_data = _with_freeipa_service_client_retry(
@@ -368,7 +368,7 @@ def get(cls, username):
368368
)
369369
if user_data is not None:
370370
cache.set(cache_key, user_data)
371-
return cls(username, user_data)
371+
return cls(username, user_data, respect_privacy=respect_privacy)
372372
except Exception as e:
373373
logger.exception(
374374
f"Failed to get user username={username}: {e}",

astra_app/core/membership_request_workflow.py

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class MembershipTarget:
6565
def _build_membership_target(membership_request: MembershipRequest) -> MembershipTarget:
6666
"""Resolve the notification target from a membership request."""
6767
if membership_request.target_kind == MembershipRequest.TargetKind.user:
68-
user = FreeIPAUser.get(membership_request.requested_username)
68+
user = FreeIPAUser.get(membership_request.requested_username, respect_privacy=False)
6969
return MembershipTarget(
7070
email=user.email if user is not None else "",
7171
email_context=user_email_context_from_user(user=user) if user is not None else {},
@@ -250,8 +250,35 @@ def _send_membership_request_notification(
250250
membership_type: MembershipType,
251251
template_name: str,
252252
extra_context: dict[str, object] | None = None,
253+
log_context: dict[str, object] | None = None,
253254
) -> object | None:
254255
if not target.email:
256+
request_id = None
257+
action = "unknown"
258+
if log_context:
259+
request_id = log_context.get("request_id")
260+
action = str(log_context.get("action") or action)
261+
warning_extra = {
262+
"event": "astra.membership.email.skipped_missing_recipient",
263+
"component": "membership",
264+
"outcome": "warning",
265+
"template_name": template_name,
266+
"target_kind": "organization" if target.target_organization is not None else "user",
267+
"target_username": target.target_username,
268+
"target_organization_id": target.target_organization.pk if target.target_organization is not None else None,
269+
}
270+
if log_context:
271+
warning_extra.update(log_context)
272+
logger.warning(
273+
"Membership email skipped because recipient email is missing request_id=%s action=%s target_kind=%s target_username=%s target_organization_id=%s template_name=%s",
274+
request_id,
275+
action,
276+
"organization" if target.target_organization is not None else "user",
277+
target.target_username,
278+
target.target_organization.pk if target.target_organization is not None else None,
279+
template_name,
280+
extra=warning_extra,
281+
)
255282
return None
256283

257284
context: dict[str, object] = {
@@ -405,7 +432,7 @@ def record_membership_request_created(
405432

406433
if send_submitted_email:
407434
try:
408-
target = FreeIPAUser.get(membership_request.requested_username)
435+
target = FreeIPAUser.get(membership_request.requested_username, respect_privacy=False)
409436
except Exception as e:
410437
logger.exception(
411438
"%s: FreeIPAUser.get failed for submitted email request_id=%s target=%r",
@@ -440,6 +467,23 @@ def record_membership_request_created(
440467
extra=current_exception_log_fields(),
441468
)
442469
email_error = e
470+
elif target is not None:
471+
logger.warning(
472+
"%s: submitted email skipped due to missing recipient address request_id=%s target=%r",
473+
log_prefix,
474+
membership_request.pk,
475+
membership_request.requested_username,
476+
extra={
477+
"event": "astra.membership.email.skipped_missing_recipient",
478+
"component": "membership",
479+
"outcome": "warning",
480+
"template_name": settings.MEMBERSHIP_REQUEST_SUBMITTED_EMAIL_TEMPLATE_NAME,
481+
"request_id": membership_request.pk,
482+
"target_kind": "user",
483+
"target_username": membership_request.requested_username,
484+
"action": "submitted",
485+
},
486+
)
443487

444488
_try_record_email_note(
445489
membership_request=membership_request,
@@ -663,6 +707,23 @@ def approve_membership_request(
663707
previous_expires_at=previous_expires_at,
664708
)
665709
_ensure_configured_email_template_exists(template_name=template_name)
710+
elif send_approved_email:
711+
logger.warning(
712+
"%s: approved email skipped due to missing recipient address request_id=%s org_id=%s",
713+
log_prefix,
714+
membership_request.pk,
715+
org.pk,
716+
extra={
717+
"event": "astra.membership.email.skipped_missing_recipient",
718+
"component": "membership",
719+
"outcome": "warning",
720+
"template_name": settings.MEMBERSHIP_REQUEST_APPROVED_EMAIL_TEMPLATE_NAME,
721+
"request_id": membership_request.pk,
722+
"target_kind": "organization",
723+
"target_organization_id": org.pk,
724+
"action": "approved",
725+
},
726+
)
666727

667728
old_membership = (
668729
Membership.objects.select_related("membership_type")
@@ -731,7 +792,7 @@ def approve_membership_request(
731792
)
732793

733794
try:
734-
user = FreeIPAUser.get(membership_request.requested_username)
795+
user = FreeIPAUser.get(membership_request.requested_username, respect_privacy=False)
735796
except Exception:
736797
logger.exception(
737798
"%s: FreeIPAUser.get failed request_id=%s target=%r",
@@ -759,6 +820,23 @@ def approve_membership_request(
759820
previous_expires_at=previous_expires_at,
760821
)
761822
_ensure_configured_email_template_exists(template_name=template_name)
823+
elif send_approved_email:
824+
logger.warning(
825+
"%s: approved email skipped due to missing recipient address request_id=%s target=%r",
826+
log_prefix,
827+
membership_request.pk,
828+
membership_request.requested_username,
829+
extra={
830+
"event": "astra.membership.email.skipped_missing_recipient",
831+
"component": "membership",
832+
"outcome": "warning",
833+
"template_name": settings.MEMBERSHIP_REQUEST_APPROVED_EMAIL_TEMPLATE_NAME,
834+
"request_id": membership_request.pk,
835+
"target_kind": "user",
836+
"target_username": membership_request.requested_username,
837+
"action": "approved",
838+
},
839+
)
762840
group_add_payload = (membership_request.requested_username, membership_type.group_cn)
763841

764842
email_context = (
@@ -978,14 +1056,19 @@ def reject_membership_request(
9781056

9791057
email_error: Exception | None = None
9801058
sent_email = None
981-
if send_rejected_email and target.email:
1059+
if send_rejected_email:
9821060
try:
9831061
sent_email = _send_membership_request_notification(
9841062
target=target,
9851063
membership_type=membership_type,
9861064
template_name=settings.MEMBERSHIP_REQUEST_REJECTED_EMAIL_TEMPLATE_NAME,
9871065
extra_context=freeform_message_email_context(key="rejection_reason", value=reason),
1066+
log_context={
1067+
"request_id": membership_request.pk,
1068+
"action": "rejected",
1069+
},
9881070
)
1071+
9891072
except Exception as e:
9901073
logger.exception(
9911074
"%s: sending rejected email failed request_id=%s",
@@ -1182,6 +1265,10 @@ def put_membership_request_on_hold(
11821265
**freeform_message_email_context(key="rfi_message", value=message),
11831266
"application_url": application_url,
11841267
},
1268+
log_context={
1269+
"request_id": membership_request.pk,
1270+
"action": "rfi",
1271+
},
11851272
)
11861273
except Exception as e:
11871274
logger.exception(
@@ -1191,6 +1278,16 @@ def put_membership_request_on_hold(
11911278
extra=current_exception_log_fields(),
11921279
)
11931280
email_error = e
1281+
elif send_rfi_email:
1282+
_send_membership_request_notification(
1283+
target=target,
1284+
membership_type=membership_type,
1285+
template_name=settings.MEMBERSHIP_REQUEST_RFI_EMAIL_TEMPLATE_NAME,
1286+
log_context={
1287+
"request_id": membership_request.pk,
1288+
"action": "rfi",
1289+
},
1290+
)
11941291

11951292
log = _create_status_change_log(
11961293
membership_request=membership_request,

astra_app/core/templates/core/_settings_tab_privacy.html

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,6 @@ <h2 class="h4">Privacy</h2>
99
<button type="submit" class="btn btn-primary">Save privacy settings</button>
1010
</form>
1111

12-
{% if privacy_warnings %}
13-
<div class="alert alert-warning" role="alert">
14-
<strong>Manual review required.</strong>
15-
<ul class="mb-0 mt-2">
16-
{% for warning in privacy_warnings %}
17-
<li>{{ warning }}</li>
18-
{% endfor %}
19-
</ul>
20-
</div>
21-
{% endif %}
22-
2312
<div class="card border-danger">
2413
<div class="card-body">
2514
<h3 class="h5">Delete my account</h3>
@@ -45,6 +34,17 @@ <h3 class="h5">Delete my account</h3>
4534
<button type="submit" class="btn btn-danger">Submit deletion request</button>
4635
</form>
4736
{% endif %}
37+
38+
{% if privacy_warnings %}
39+
<div class="alert alert-warning mt-3" role="alert">
40+
<strong>Manual review required.</strong>
41+
<ul class="mb-0 mt-2">
42+
{% for warning in privacy_warnings %}
43+
<li>{{ warning }}</li>
44+
{% endfor %}
45+
</ul>
46+
</div>
47+
{% endif %}
4848
</div>
4949
</div>
5050
</div>

astra_app/core/templates/core/user_profile.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@
7979

8080
<h3 class="profile-username mb-1">{{ fu.get_full_name }}</h3>
8181
<div class="text-muted" id="user_username">{{ fu.username }}</div>
82-
{% if fu.email %}
83-
<div class="mt-1" id="user_mail"><a href="mailto:{{ fu.email }}">{{ fu.email }}</a></div>
82+
{% if profile_email %}
83+
<div class="mt-1" id="user_mail"><a href="mailto:{{ profile_email }}">{{ profile_email }}</a></div>
8484
{% endif %}
8585

8686
{% if is_self %}

astra_app/core/tests/test_fasisprivate_anonymize.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,55 @@ def test_user_profile_shows_membership_card_for_private_profile_committee_viewer
222222
html = resp.content.decode("utf-8")
223223
self.assertIn("Membership</strong>", html)
224224

225+
def test_user_profile_shows_email_for_private_profile_committee_viewer(self) -> None:
226+
factory = RequestFactory()
227+
request = factory.get("/user/bob/")
228+
request.user = SimpleNamespace(
229+
is_authenticated=True,
230+
get_username=lambda: "reviewer",
231+
groups_list=[settings.FREEIPA_MEMBERSHIP_COMMITTEE_GROUP],
232+
)
233+
234+
set_current_viewer_username("reviewer")
235+
try:
236+
private_profile = FreeIPAUser(
237+
"bob",
238+
{
239+
"uid": ["bob"],
240+
"givenname": ["Bob"],
241+
"sn": ["User"],
242+
"mail": ["bob@example.org"],
243+
"fasIsPrivate": ["TRUE"],
244+
"memberof_group": ["packagers"],
245+
},
246+
)
247+
finally:
248+
clear_current_viewer_username()
249+
250+
full_profile = FreeIPAUser(
251+
"bob",
252+
{
253+
"uid": ["bob"],
254+
"givenname": ["Bob"],
255+
"sn": ["User"],
256+
"mail": ["bob@example.org"],
257+
"fasIsPrivate": ["TRUE"],
258+
"memberof_group": ["packagers"],
259+
},
260+
)
261+
262+
with (
263+
patch("core.views_users._get_full_user", autospec=True, return_value=private_profile),
264+
patch("core.views_users.FreeIPAUser.get", autospec=True, return_value=full_profile),
265+
patch("core.views_users.FreeIPAGroup.all", autospec=True, return_value=[]),
266+
patch("core.views_users.has_enabled_agreements", autospec=True, return_value=False),
267+
):
268+
resp = views_users.user_profile(request, "bob")
269+
270+
self.assertEqual(resp.status_code, 200)
271+
html = resp.content.decode("utf-8")
272+
self.assertIn("bob@example.org", html)
273+
225274
def test_user_profile_shows_membership_card_for_private_profile_committee_viewer_without_groups_list(self) -> None:
226275
factory = RequestFactory()
227276
request = factory.get("/user/bob/")
@@ -256,7 +305,11 @@ def test_user_profile_shows_membership_card_for_private_profile_committee_viewer
256305

257306
with (
258307
patch("core.views_users._get_full_user", autospec=True, return_value=bob),
259-
patch("core.views_users.FreeIPAUser.get", autospec=True, side_effect=lambda username: reviewer if username == "reviewer" else None),
308+
patch(
309+
"core.views_users.FreeIPAUser.get",
310+
autospec=True,
311+
side_effect=lambda username, **_kwargs: reviewer if username == "reviewer" else None,
312+
),
260313
patch("core.views_users.FreeIPAGroup.all", autospec=True, return_value=[]),
261314
patch("core.views_users.has_enabled_agreements", autospec=True, return_value=False),
262315
):

0 commit comments

Comments
 (0)