Skip to content

Commit 62aa943

Browse files
feat: record client type in session audit events
1 parent adcfc6b commit 62aa943

3 files changed

Lines changed: 44 additions & 0 deletions

File tree

src/apps/audit/domain.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class AuditEvent(PublicModel):
3939
4040
Applicable to HTTP requests:
4141
- client_ip
42+
- client_source
4243
- http_request_id
4344
- http_request_method,
4445
- http_response_status_code
@@ -92,6 +93,7 @@ class AuditEvent(PublicModel):
9293

9394
# For HTTP requests
9495
client_ip: Annotated[str | None, Field(alias="client.ip")] = None
96+
client_source: Annotated[str | None, Field(alias="client.source")] = None # Mindlogger-Content-Source header
9597
http_request_id: Annotated[str | None, Field(alias="http.request.id")] = None # from asgi-correlation-id
9698
http_request_method: Annotated[str | None, Field(alias="http.request.method")] = None
9799
http_response_status_code: Annotated[int | None, Field(alias="http.response.status_code")] = None

src/apps/audit/fields.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def http_audit_fields(request: Request, error: BaseError | StarletteHTTPExceptio
1515
span = tracer.current_span()
1616
fields = {
1717
"client_ip": request.client and request.client.host,
18+
"client_source": request.headers.get("mindlogger-content-source"),
1819
"http_request_id": correlation_id.get(),
1920
"http_request_method": request.method,
2021
"http_response_status_code": isinstance(route, APIRoute) and route.status_code or 200,

src/apps/authentication/tests/test_auth.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,47 @@ async def test_login_embeds_client_claim(self, client: TestClient, user: User, c
446446
assert refresh_payload["client"] == content_source
447447
self._assert_lifetimes_unchanged(access_payload, refresh_payload, before, after)
448448

449+
async def test_login_audit_event_records_client_source(self, client: TestClient, user: User, mocker: MockerFixture):
450+
audit_log = mocker.patch("apps.authentication.api.auth.log")
451+
resp = await client.post(
452+
self.get_token_url,
453+
data={"email": user.email_encrypted, "password": TEST_PASSWORD},
454+
headers={"Mindlogger-Content-Source": "admin"},
455+
)
456+
assert resp.status_code == http.HTTPStatus.OK
457+
event = audit_log.call_args[0][0]
458+
assert event.client_source == "admin"
459+
460+
async def test_login_audit_event_without_client_source(self, client: TestClient, user: User, mocker: MockerFixture):
461+
audit_log = mocker.patch("apps.authentication.api.auth.log")
462+
resp = await client.post(
463+
self.get_token_url,
464+
data={"email": user.email_encrypted, "password": TEST_PASSWORD},
465+
)
466+
assert resp.status_code == http.HTTPStatus.OK
467+
event = audit_log.call_args[0][0]
468+
assert event.client_source is None
469+
470+
async def test_refresh_audit_event_records_client_source(
471+
self, client: TestClient, user: User, mocker: MockerFixture
472+
):
473+
audit_log = mocker.patch("apps.authentication.api.auth.log")
474+
refresh_token = AuthenticationService.create_refresh_token(
475+
{
476+
"sub": str(user.id),
477+
"jti": str(uuid.uuid4()),
478+
"client": "web",
479+
}
480+
)
481+
resp = await client.post(
482+
auth_router.url_path_for("refresh_access_token"),
483+
data={"refresh_token": refresh_token},
484+
headers={"Mindlogger-Content-Source": "web"},
485+
)
486+
assert resp.status_code == http.HTTPStatus.OK
487+
event = audit_log.call_args[0][0]
488+
assert event.client_source == "web"
489+
449490
@pytest.mark.parametrize(
450491
"headers",
451492
(None, {"Mindlogger-Content-Source": "invalid-content-source"}),

0 commit comments

Comments
 (0)