208 lines
6.3 KiB
Python
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"
|