Files
ctms/backend/tests/test_audit_access_context.py
T
Cheng Zhou f11a5c84d9
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
feat(监控): 完善系统监控、登录状态与访问审计能力
2026-07-10 17:12:13 +08:00

208 lines
6.3 KiB
Python

import uuid
import pytest
from app.core.request_context import (
RequestAuditContext,
build_request_audit_context,
reset_request_audit_context,
set_request_audit_context,
)
from app.crud.audit import log_action
class FakeAsyncSession:
def __init__(self) -> None:
self.added = None
self.flushed = False
def add(self, value) -> None:
self.added = value
async def flush(self) -> None:
self.flushed = True
class FakeClient:
def __init__(self, host: str = "10.0.0.8") -> None:
self.host = host
self.port = 54321
class FakeUrl:
scheme = "https"
path = "/api/v1/audit"
query = "token=secret-token&view=all&subject_name=Alice"
class FakeQueryParams:
def __init__(self) -> None:
self._items = [
("token", "secret-token"),
("view", "all"),
("subject_name", "Alice"),
]
def __bool__(self) -> bool:
return True
def multi_items(self):
return list(self._items)
class FakeRequest:
def __init__(self, headers: dict[str, str], *, client_host: str = "10.0.0.8") -> None:
self.headers = headers
self.client = FakeClient(client_host)
self.method = "GET"
self.url = FakeUrl()
self.query_params = FakeQueryParams()
self.scope = {
"http_version": "1.1",
"scheme": "https",
"path": "/api/v1/audit",
"method": "GET",
}
@pytest.mark.asyncio
async def test_audit_log_action_captures_request_access_context():
context_token = set_request_audit_context(
RequestAuditContext(
client_ip="203.0.113.10",
user_agent="curl/8.1.2",
client_type="desktop",
client_version="1.2.3",
client_platform="macos",
build_channel="release",
build_commit="abcdef1",
)
)
db = FakeAsyncSession()
try:
log = await log_action(
db,
study_id=uuid.uuid4(),
entity_type="subject",
entity_id=uuid.uuid4(),
action="UPDATE_SUBJECT",
detail=None,
operator_id=uuid.uuid4(),
operator_role="PM",
auto_commit=False,
)
finally:
reset_request_audit_context(context_token)
assert db.added is log
assert db.flushed is True
assert log.client_ip == "203.0.113.10"
assert log.user_agent == "curl/8.1.2"
assert log.client_type == "desktop"
assert log.client_version == "1.2.3"
assert log.client_platform == "macos"
assert log.build_channel == "release"
assert log.build_commit == "abcdef1"
def test_request_audit_context_captures_sanitized_headers():
context = build_request_audit_context(
FakeRequest(
{
"user-agent": "Chrome",
"referer": "https://ctms.local/audit?token=secret-token",
"authorization": "Bearer secret-token",
"cookie": "sid=secret",
"x-ctms-client-source": "ctms-web",
"x-ctms-client-type": "web",
"x-ctms-client-version": "0.1.0",
"x-request-id": "req-123",
"x-custom-secret": "hidden",
"x-random-debug": "skip-me",
}
)
)
assert context.user_agent == "Chrome"
assert context.client_type == "web"
assert context.client_ip == "10.0.0.8"
assert uuid.UUID(context.request_id)
assert context.request_headers == {
"authorization": "[redacted]",
"cookie": "[redacted]",
"user-agent": "Chrome",
"x-ctms-client-source": "ctms-web",
"x-ctms-client-type": "web",
"x-ctms-client-version": "0.1.0",
"x-custom-secret": "[redacted]",
"x-request-id": "req-123",
}
assert context.request_snapshot is not None
assert context.request_snapshot["method"] == "GET"
assert context.request_snapshot["request_id"] == context.request_id
assert context.request_snapshot["path"] == "/api/v1/audit"
assert context.request_snapshot["query_string"] == (
"token=[redacted]&view=all&subject_name=[redacted]"
)
assert context.request_snapshot["query_params"] == [
{"name": "token", "value": "[redacted]"},
{"name": "view", "value": "all"},
{"name": "subject_name", "value": "[redacted]"},
]
assert context.request_snapshot["headers"]["authorization"] == "[redacted]"
assert context.request_snapshot["headers"]["cookie"] == "[redacted]"
assert context.request_snapshot["headers"]["x-ctms-client-source"] == "ctms-web"
assert "referer" not in context.request_snapshot["headers"]
assert "x-random-debug" not in context.request_snapshot["headers"]
assert context.request_snapshot["client"] == {"host": "10.0.0.8", "port": 54321}
assert context.request_snapshot["body"] == {
"captured": False,
"reason": "body_not_captured_by_audit_policy",
}
def test_request_audit_context_prefers_explicit_ctms_client_source():
context = build_request_audit_context(
FakeRequest(
{
"x-ctms-client-source": "ctms-desktop",
"x-ctms-client-type": "web",
}
)
)
assert context.client_type == "desktop"
assert context.request_headers == {
"x-ctms-client-source": "ctms-desktop",
"x-ctms-client-type": "web",
}
def test_request_audit_context_uses_proxy_observed_ip_instead_of_spoofed_forwarded_prefix(monkeypatch):
monkeypatch.setattr("app.core.request_context.settings.TRUSTED_PROXY_CIDRS", "10.0.0.0/8")
context = build_request_audit_context(
FakeRequest(
{
"x-real-ip": "203.0.113.20",
"x-forwarded-for": "1.2.3.4, 203.0.113.20",
}
)
)
assert context.client_ip == "203.0.113.20"
def test_request_audit_context_ignores_forwarded_headers_from_untrusted_peer(monkeypatch):
monkeypatch.setattr("app.core.request_context.settings.TRUSTED_PROXY_CIDRS", "10.0.0.0/8")
context = build_request_audit_context(
FakeRequest(
{
"x-real-ip": "203.0.113.20",
"x-forwarded-for": "1.2.3.4",
},
client_host="198.51.100.8",
)
)
assert context.client_ip == "198.51.100.8"