Skip to content

Commit 9e1f51a

Browse files
docs: document account-level export scoping and add cross-applet test
1 parent a111cf8 commit 9e1f51a

3 files changed

Lines changed: 30 additions & 0 deletions

File tree

src/apps/audit/crud.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ async def search_applet_events(
5454
page: int = 1,
5555
limit: int = DEFAULT_PAGE_SIZE,
5656
) -> tuple[list[AuditLogSchema], int]:
57+
"""Events for an applet's audit export.
58+
59+
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.
63+
"""
5764
# Users with a manager-class role on this applet. Their account-level events
5865
# (login/logout/MFA/...) are surfaced in the export even though those events
5966
# carry no applet id; respondents are intentionally excluded.

src/apps/audit/db/schemas.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class AuditLogSchema(Base):
1010
Append-only. A few columns are denormalized from the ECS document for
1111
filtering/sorting; the full event is kept in ``payload`` as the same
1212
dotted-alias JSON that is produced by ``AuditEvent.model_dump(mode="json")``.
13+
14+
Note: for failed logins the denormalized ``user_id`` is resolved from the
15+
attempted email at persist time and may therefore be set even though
16+
``payload["user.id"]`` stays ``null`` (see ``tasks._resolve_actor_from_email``).
1317
"""
1418

1519
__tablename__ = "audit_logs"

src/apps/audit/tests/test_query_service.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,25 @@ async def test_mixes_applet_and_account_events(session: AsyncSession, tom: User,
158158
assert [e.event_id for e in events] == [account_event.event_id, applet_event.event_id]
159159

160160

161+
async def test_account_event_isolated_across_applets(
162+
session: AsyncSession,
163+
tom: User,
164+
lucy: User,
165+
applet_one_lucy_manager: AppletFull,
166+
applet_two: AppletFull,
167+
):
168+
"""An account event surfaces only in exports of applets the user is privileged
169+
on. lucy manages applet_one but has no role on applet_two, so her login must
170+
not leak into applet_two's export."""
171+
await _seed(session, _make_account_event(user_id=lucy.id, timestamp=datetime.datetime(2026, 5, 1, 10, 0, 0)))
172+
173+
_, total_one = await AuditQueryService(session).search_applet_events(applet_one_lucy_manager.id)
174+
_, total_two = await AuditQueryService(session).search_applet_events(applet_two.id)
175+
176+
assert total_one == 1
177+
assert total_two == 0
178+
179+
161180
async def test_save_is_idempotent_on_event_id(session: AsyncSession):
162181
applet_id = uuid.uuid4()
163182
event = _make_event(applet_id, timestamp=datetime.datetime(2026, 5, 1, 10, 0, 0))

0 commit comments

Comments
 (0)