Skip to content

Commit 9031a50

Browse files
authored
fix: Clean up applet IAM events (M2-10745) (#2090)
🔗 [Jira Ticket M2-10745](https://mindlogger.atlassian.net/browse/M2-10745) Changes include: - Use `user.target.roles=null` instead of `user.target.roles=["shell-account"]` for shell account invite - Use `user.target.id` instead of `user.target.email` when possible - Log `user.target.id` or `user.target.email` when transferring owner - Log `user.target.id` when accepting invites These fix a few small issues I noticed while reviewing applet IAM audit log events.
1 parent 052a023 commit 9031a50

7 files changed

Lines changed: 167 additions & 44 deletions

File tree

src/apps/audit/tests/test_tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def _payload(**overrides) -> dict:
1212
event = AuditEvent(
1313
event_action=EventAction.USER_SESSION_INVALID,
1414
user_id=None,
15-
user_email="tom@mindlogger.com",
15+
user_email="tom@gettingcurious.com",
1616
)
1717
payload = event.model_dump(mode="json")
1818
payload.update(overrides)

src/apps/invitations/api.py

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -100,22 +100,27 @@ async def invitation_respondent_send(
100100
for the concrete user giving him a role "respondent".
101101
"""
102102

103+
invited_user_id: uuid.UUID | None = None
103104
try:
104105
async with atomic(session):
106+
try:
107+
# Resolve invited user first so failure audit events carry user ID
108+
invited_user = await UserService(session).get_by_email(invitation_schema.email)
109+
invited_user_id = invited_user.id
110+
except UserNotFound:
111+
# Inviting an email that is not yet associated with a user is valid.
112+
# Continue flow to create subject and send invitation.
113+
pass
114+
105115
await AppletService(session, user.id).exist_by_id(applet_id)
106116
await CheckAccessService(session, user.id).check_applet_invite_access(applet_id)
107117
invitation_service = InvitationsService(session, user)
108-
try:
109-
invited_user = await UserService(session).get_by_email(invitation_schema.email)
110-
is_role_exist = await UserAppletAccessService(session, invited_user.id, applet_id).has_role(
118+
if invited_user_id:
119+
is_role_exist = await UserAppletAccessService(session, invited_user_id, applet_id).has_role(
111120
Role.RESPONDENT
112121
)
113122
if is_role_exist:
114123
raise RespondentInvitationExist()
115-
except UserNotFound:
116-
# Inviting an email that is not yet associated with a user is valid.
117-
# Continue flow to create subject and send invitation.
118-
pass
119124

120125
subject_service = SubjectsService(session, user.id)
121126
try:
@@ -134,7 +139,8 @@ async def invitation_respondent_send(
134139
event_action=EventAction.APPLET_INVITE_INITIATE,
135140
user_id=user.id,
136141
curious_applet_id=[applet_id],
137-
user_target_email=invitation_schema.email,
142+
user_target_id=invited_user_id,
143+
user_target_email=None if invited_user_id else invitation_schema.email,
138144
user_target_roles=[Role.RESPONDENT],
139145
**http_audit_fields(request, e),
140146
)
@@ -146,7 +152,8 @@ async def invitation_respondent_send(
146152
event_action=EventAction.APPLET_INVITE_INITIATE,
147153
user_id=user.id,
148154
curious_applet_id=[applet_id],
149-
user_target_email=invitation_schema.email,
155+
user_target_id=invited_user_id,
156+
user_target_email=None if invited_user_id else invitation_schema.email,
150157
user_target_roles=[Role.RESPONDENT],
151158
**http_audit_fields(request),
152159
)
@@ -166,22 +173,27 @@ async def invitation_reviewer_send(
166173
for the concrete user giving him role "reviewer" for specific respondents.
167174
"""
168175

176+
invited_user_id: uuid.UUID | None = None
169177
try:
170178
async with atomic(session):
179+
try:
180+
# Resolve invited user first so failure audit events carry user ID
181+
invited_user = await UserService(session).get_by_email(invitation_schema.email)
182+
invited_user_id = invited_user.id
183+
except UserNotFound:
184+
# Inviting by email is allowed even if the user does not exist yet.
185+
# Continue so the invitation can be created for later acceptance/registration.
186+
pass
187+
171188
await AppletService(session, user.id).exist_by_id(applet_id)
172189
await CheckAccessService(session, user.id).check_applet_invite_access(applet_id)
173190
invitation_srv = InvitationsService(session, user)
174-
try:
175-
invited_user = await UserService(session).get_by_email(invitation_schema.email)
176-
is_role_exist = await UserAppletAccessService(session, invited_user.id, applet_id).has_role(
191+
if invited_user_id:
192+
is_role_exist = await UserAppletAccessService(session, invited_user_id, applet_id).has_role(
177193
Role.REVIEWER
178194
)
179195
if is_role_exist:
180196
raise ManagerInvitationExist()
181-
except UserNotFound:
182-
# Inviting by email is allowed even if the user does not exist yet.
183-
# Continue so the invitation can be created for later acceptance/registration.
184-
invited_user = None
185197

186198
invitation: InvitationDetailForReviewer = await invitation_srv.send_reviewer_invitation(
187199
applet_id, invitation_schema
@@ -192,7 +204,8 @@ async def invitation_reviewer_send(
192204
event_action=EventAction.APPLET_INVITE_INITIATE,
193205
user_id=user.id,
194206
curious_applet_id=[applet_id],
195-
user_target_email=invitation_schema.email,
207+
user_target_id=invited_user_id,
208+
user_target_email=None if invited_user_id else invitation_schema.email,
196209
user_target_roles=[Role.REVIEWER],
197210
**http_audit_fields(request, e),
198211
)
@@ -204,7 +217,8 @@ async def invitation_reviewer_send(
204217
event_action=EventAction.APPLET_INVITE_INITIATE,
205218
user_id=user.id,
206219
curious_applet_id=[applet_id],
207-
user_target_email=invitation_schema.email,
220+
user_target_id=invited_user_id,
221+
user_target_email=None if invited_user_id else invitation_schema.email,
208222
user_target_roles=[Role.REVIEWER],
209223
**http_audit_fields(request),
210224
)
@@ -225,22 +239,27 @@ async def invitation_managers_send(
225239
"manager", "coordinator", "editor".
226240
"""
227241

242+
invited_user_id: uuid.UUID | None = None
228243
try:
229244
async with atomic(session):
245+
try:
246+
# Resolve invited user first so failure audit events carry user ID
247+
invited_user = await UserService(session).get_by_email(invitation_schema.email)
248+
invited_user_id = invited_user.id
249+
except UserNotFound:
250+
# Inviting emails that are not yet associated with a user is allowed.
251+
# In this case there is no existing user-role assignment to validate.
252+
pass
253+
230254
await AppletService(session, user.id).exist_by_id(applet_id)
231255
await CheckAccessService(session, user.id).check_applet_invite_access(applet_id)
232256
invitation_srv = InvitationsService(session, user)
233-
try:
234-
invited_user = await UserService(session).get_by_email(invitation_schema.email)
235-
is_role_exist = await UserAppletAccessService(session, invited_user.id, applet_id).has_role(
257+
if invited_user_id:
258+
is_role_exist = await UserAppletAccessService(session, invited_user_id, applet_id).has_role(
236259
invitation_schema.role
237260
)
238261
if is_role_exist:
239262
raise ManagerInvitationExist()
240-
except UserNotFound:
241-
# Inviting emails that are not yet associated with a user is allowed.
242-
# In this case there is no existing user-role assignment to validate.
243-
pass
244263

245264
invitation = await invitation_srv.send_managers_invitation(applet_id, invitation_schema)
246265
except BaseError as e:
@@ -249,7 +268,8 @@ async def invitation_managers_send(
249268
event_action=EventAction.APPLET_INVITE_INITIATE,
250269
user_id=user.id,
251270
curious_applet_id=[applet_id],
252-
user_target_email=invitation_schema.email,
271+
user_target_id=invited_user_id,
272+
user_target_email=None if invited_user_id else invitation_schema.email,
253273
user_target_roles=[invitation_schema.role],
254274
**http_audit_fields(request, e),
255275
)
@@ -261,7 +281,8 @@ async def invitation_managers_send(
261281
event_action=EventAction.APPLET_INVITE_INITIATE,
262282
user_id=user.id,
263283
curious_applet_id=[applet_id],
264-
user_target_email=invitation_schema.email,
284+
user_target_id=invited_user_id,
285+
user_target_email=None if invited_user_id else invitation_schema.email,
265286
user_target_roles=[invitation_schema.role],
266287
**http_audit_fields(request),
267288
)
@@ -277,11 +298,13 @@ async def invitation_accept(
277298
):
278299
"""General endpoint to approve the applet invitation."""
279300
applet_id = None
301+
invitation_role = None
280302
try:
281303
invitation_service = InvitationsService(session, user)
282304
invitation = await invitation_service.get(key)
283305
if invitation:
284306
applet_id = invitation.applet_id
307+
invitation_role = invitation.role
285308

286309
try:
287310
async with atomic(session):
@@ -321,6 +344,8 @@ async def invitation_accept(
321344
event_action=EventAction.APPLET_INVITE_ACCEPT,
322345
user_id=user.id,
323346
curious_applet_id=[applet_id] if applet_id else None,
347+
user_target_id=user.id,
348+
user_target_roles=[invitation_role] if invitation_role else None,
324349
**http_audit_fields(request, e),
325350
)
326351
)
@@ -331,6 +356,8 @@ async def invitation_accept(
331356
event_action=EventAction.APPLET_INVITE_ACCEPT,
332357
user_id=user.id,
333358
curious_applet_id=[applet_id],
359+
user_target_id=user.id,
360+
user_target_roles=[invitation_role] if invitation_role else None,
334361
**http_audit_fields(request),
335362
)
336363
)
@@ -343,11 +370,13 @@ async def private_invitation_accept(
343370
session=Depends(get_session),
344371
):
345372
applet_id = None
373+
invitation_role = None
346374
try:
347375
private_service = PrivateInvitationService(session)
348376
invitation = await private_service.get_invitation(key)
349377
if invitation:
350378
applet_id = invitation.applet_id
379+
invitation_role = invitation.role
351380

352381
async with atomic(session):
353382
await private_service.accept_invitation(user, key)
@@ -357,6 +386,8 @@ async def private_invitation_accept(
357386
event_action=EventAction.APPLET_INVITE_ACCEPT,
358387
user_id=user.id,
359388
curious_applet_id=[applet_id] if applet_id else None,
389+
user_target_id=user.id,
390+
user_target_roles=[invitation_role] if invitation_role else None,
360391
**http_audit_fields(request, e),
361392
)
362393
)
@@ -367,6 +398,8 @@ async def private_invitation_accept(
367398
event_action=EventAction.APPLET_INVITE_ACCEPT,
368399
user_id=user.id,
369400
curious_applet_id=[applet_id],
401+
user_target_id=user.id,
402+
user_target_roles=[invitation_role] if invitation_role else None,
370403
**http_audit_fields(request),
371404
)
372405
)
@@ -394,6 +427,7 @@ async def invitation_decline(
394427
event_action=EventAction.APPLET_INVITE_DECLINE,
395428
user_id=user.id,
396429
curious_applet_id=[applet_id] if applet_id else None,
430+
user_target_id=user.id,
397431
**http_audit_fields(request, e),
398432
)
399433
)
@@ -404,6 +438,7 @@ async def invitation_decline(
404438
event_action=EventAction.APPLET_INVITE_DECLINE,
405439
user_id=user.id,
406440
curious_applet_id=[applet_id],
441+
user_target_id=user.id,
407442
**http_audit_fields(request),
408443
)
409444
)
@@ -416,8 +451,17 @@ async def invitation_subject_send(
416451
schema: ShellAccountInvitation = Body(...),
417452
session=Depends(get_session),
418453
) -> Response[InvitationRespondentResponse]:
454+
invited_user_id: uuid.UUID | None = None
419455
try:
420456
async with atomic(session):
457+
try:
458+
# Resolve invited user first so failure audit events carry user ID
459+
invited_user = await UserService(session).get_by_email(schema.email)
460+
invited_user_id = invited_user.id
461+
except UserNotFound:
462+
# Expected: invitee may not have an account yet; proceed with invitation flow.
463+
pass
464+
421465
await AppletService(session, user.id).exist_by_id(applet_id)
422466
await CheckAccessService(session, user.id).check_applet_invite_access(applet_id)
423467

@@ -430,16 +474,12 @@ async def invitation_subject_send(
430474

431475
# check role exists
432476
invitation_service = InvitationsService(session, user)
433-
try:
434-
invited_user = await UserService(session).get_by_email(schema.email)
435-
is_role_exist = await UserAppletAccessService(session, invited_user.id, applet_id).has_role(
477+
if invited_user_id:
478+
is_role_exist = await UserAppletAccessService(session, invited_user_id, applet_id).has_role(
436479
Role.RESPONDENT
437480
)
438481
if is_role_exist:
439482
raise RespondentInvitationExist()
440-
except UserNotFound:
441-
# Expected: invitee may not have an account yet; proceed with invitation flow.
442-
pass
443483

444484
invitation_schema = InvitationRespondentRequest(
445485
email=schema.email,
@@ -460,7 +500,8 @@ async def invitation_subject_send(
460500
event_action=EventAction.APPLET_INVITE_INITIATE,
461501
user_id=user.id,
462502
curious_applet_id=[applet_id],
463-
user_target_email=schema.email,
503+
user_target_id=invited_user_id,
504+
user_target_email=None if invited_user_id else schema.email,
464505
user_target_roles=[Role.RESPONDENT],
465506
**http_audit_fields(request, e),
466507
)
@@ -472,7 +513,8 @@ async def invitation_subject_send(
472513
event_action=EventAction.APPLET_INVITE_INITIATE,
473514
user_id=user.id,
474515
curious_applet_id=[applet_id],
475-
user_target_email=schema.email,
516+
user_target_id=invited_user_id,
517+
user_target_email=None if invited_user_id else schema.email,
476518
user_target_roles=[Role.RESPONDENT],
477519
**http_audit_fields(request),
478520
)

src/apps/invitations/router.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async def create_shell_account(
164164
user_id=user.id,
165165
curious_applet_id=[applet_id],
166166
user_target_email=subject_schema.email,
167-
user_target_roles=["shell-account"],
167+
# `user_target_roles=None` because shell account grants no role
168168
**http_audit_fields(request, e),
169169
)
170170
)
@@ -176,7 +176,6 @@ async def create_shell_account(
176176
user_id=user.id,
177177
curious_applet_id=[applet_id],
178178
user_target_email=subject_schema.email,
179-
user_target_roles=["shell-account"],
180179
**http_audit_fields(request),
181180
)
182181
)

0 commit comments

Comments
 (0)