Skip to content

Commit 91b4cbf

Browse files
authored
feat: Export user:* events from past applet membership (M2-10698) (#2093)
[Jira Ticket M2-10698](https://mindlogger.atlassian.net/browse/M2-10698) Changes include: - Export `user:*` events from past applet membership - Exclude events that occur after applet membership ended We do not store membership history, so events from older applet memberships are not included in the export.
1 parent 50f852f commit 91b4cbf

2 files changed

Lines changed: 41 additions & 10 deletions

File tree

src/apps/audit/crud.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,29 @@ async def search_applet_events(
5757
"""Events for an applet's audit export.
5858
5959
Returns the applet's own events plus account-level events
60-
(``ACCOUNT_LEVEL_EXPORT_ACTIONS``) for its manager-class users. Membership
61-
is resolved at query time against current roles, so an account event stops
62-
appearing once the user loses their role on the applet, and events from
63-
before the user's access was granted are never surfaced.
60+
(``ACCOUNT_LEVEL_EXPORT_ACTIONS``) for its manager-class users. Events
61+
for current members are included from the time the membership started.
62+
Events for past members are included for the time period of the most
63+
recent past membership. (Note that we do not store membership history
64+
beyond the most recent past membership, so events during older past
65+
memberships will not be included.)
6466
"""
6567
# Users with a manager-class role on this applet. Their account-level events
6668
# (login/logout/MFA/...) are surfaced in the export even though those events
6769
# carry no applet id; respondents are intentionally excluded. Only events
68-
# from the access grant onwards qualify — adding a member must not expose
69-
# their earlier session history (both columns are naive UTC).
70+
# inside the most recent membership window qualify — adding a member must
71+
# not expose their earlier session history.
7072
privileged_access = (
7173
select(UserAppletAccessSchema.id)
7274
.where(
7375
UserAppletAccessSchema.applet_id == applet_id,
7476
UserAppletAccessSchema.role.in_(Role.managers()),
75-
UserAppletAccessSchema.soft_exists(),
7677
UserAppletAccessSchema.user_id == AuditLogSchema.user_id,
7778
UserAppletAccessSchema.created_at <= AuditLogSchema.event_timestamp,
79+
or_(
80+
UserAppletAccessSchema.soft_exists(),
81+
AuditLogSchema.event_timestamp <= UserAppletAccessSchema.updated_at,
82+
),
7883
)
7984
.exists()
8085
)

src/apps/audit/tests/test_api.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,13 @@ async def test_owner_does_not_see_manager_account_events_from_before_joining(
217217
assert body["result"][0]["event.id"] == str(after_join.event_id)
218218

219219

220-
async def test_owner_does_not_see_revoked_manager_account_events(
220+
async def test_owner_sees_revoked_manager_account_events_from_during_membership(
221221
client: TestClient, session: AsyncSession, tom: User, lucy: User, applet_one_lucy_manager: AppletFull
222222
):
223+
"""Removing a team member keeps their account events from while they were
224+
a member visible in the export."""
223225
applet = applet_one_lucy_manager
224-
# After the grant, so revocation alone is what hides it.
225-
await _seed_account_event(session, user_id=lucy.id, timestamp=_utcnow() + datetime.timedelta(hours=1))
226+
during = await _seed_account_event(session, user_id=lucy.id, timestamp=_utcnow())
226227

227228
# Revoke lucy's access (role removal is a soft delete).
228229
await session.execute(
@@ -231,6 +232,31 @@ async def test_owner_does_not_see_revoked_manager_account_events(
231232
.values(is_deleted=True)
232233
)
233234

235+
client.login(tom)
236+
response = await client.get(URL.format(applet_id=applet.id))
237+
assert response.status_code == http.HTTPStatus.OK
238+
body = response.json()
239+
assert body["count"] == 1
240+
assert body["result"][0]["event.id"] == str(during.event_id)
241+
242+
243+
async def test_owner_does_not_see_revoked_manager_account_events_from_after_removal(
244+
client: TestClient, session: AsyncSession, tom: User, lucy: User, applet_one_lucy_manager: AppletFull
245+
):
246+
"""Removing a team member ends their membership window: account events
247+
from after the revocation stay out of the export."""
248+
applet = applet_one_lucy_manager
249+
250+
# Timestamp event an hour after the revocation below.
251+
await _seed_account_event(session, user_id=lucy.id, timestamp=_utcnow() + datetime.timedelta(hours=1))
252+
253+
# Revoke lucy's access (role removal is a soft delete that bumps updated_at).
254+
await session.execute(
255+
update(UserAppletAccessSchema)
256+
.where(UserAppletAccessSchema.user_id == lucy.id, UserAppletAccessSchema.applet_id == applet.id)
257+
.values(is_deleted=True)
258+
)
259+
234260
client.login(tom)
235261
response = await client.get(URL.format(applet_id=applet.id))
236262
assert response.status_code == http.HTTPStatus.OK

0 commit comments

Comments
 (0)