Skip to content

Commit e14c6c3

Browse files
committed
MPP-4012 - feat(glean): log API access as Glean server event
Introduce a new `api.accessed` Glean event to capture accesses to Relay API endpoints. This includes the HTTP method and endpoint path, and logs events for all `/api/` prefixed routes via a new middleware component. - Added `record_api_accessed()` to `EventsServerEventLogger` - Extended `RelayGleanLogger` with `log_api_accessed()` for easier integration - Registered `GleanApiAccessMiddleware` to log access for all API routes - Added corresponding unit test for API access logging - Updated `relay-server-metrics.yaml` to define the `api.accessed` metric - Updated notification email for several existing metrics to use [email protected]
1 parent 35681e1 commit e14c6c3

File tree

7 files changed

+134
-9
lines changed

7 files changed

+134
-9
lines changed

privaterelay/glean/server_events.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,37 @@ def emit_record(self, now: datetime, ping: dict[str, Any]) -> None:
9090

9191
print(ping_envelope_serialized)
9292

93+
def record_api_accessed(
94+
self,
95+
user_agent: str,
96+
ip_address: str,
97+
endpoint: str,
98+
method: str,
99+
fxa_id: str,
100+
) -> None:
101+
"""
102+
Record and submit a api_accessed event:
103+
An API endpoint was accessed.
104+
Event is logged to STDOUT via `print`.
105+
106+
:param str user_agent: The user agent.
107+
:param str ip_address: The IP address. Will be used to decode Geo information
108+
and scrubbed at ingestion.
109+
:param str endpoint: The name of the endpoint accessed
110+
:param str method: HTTP method used
111+
:param str fxa_id: Mozilla accounts user ID
112+
"""
113+
event = {
114+
"category": "api",
115+
"name": "accessed",
116+
"extra": {
117+
"endpoint": str(endpoint),
118+
"method": str(method),
119+
"fxa_id": str(fxa_id),
120+
},
121+
}
122+
self._record(user_agent, ip_address, event)
123+
93124
def record_email_blocked(
94125
self,
95126
user_agent: str,

privaterelay/glean_interface.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,17 @@ def log_email_blocked(
298298
is_reply=is_reply,
299299
reason=reason,
300300
)
301+
302+
def log_api_accessed(self, request: HttpRequest) -> None:
303+
"""Log that any Relay API endpoint was accessed."""
304+
if not request.user or not request.user.is_authenticated:
305+
return
306+
request_data = RequestData.from_request(request)
307+
user_data = UserData.from_user(request.user)
308+
self.record_api_accessed(
309+
user_agent=_opt_str_to_glean(request_data.user_agent),
310+
ip_address=_opt_str_to_glean(request_data.ip_address),
311+
endpoint=request.path,
312+
method=_opt_str_to_glean(request.method),
313+
fxa_id=_opt_str_to_glean(user_data.fxa_id),
314+
)

privaterelay/middleware.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from csp.middleware import CSPMiddleware
1414
from whitenoise.middleware import WhiteNoiseMiddleware
1515

16+
from privaterelay.utils import glean_logger
17+
1618
metrics = markus.get_metrics()
1719

1820

@@ -197,3 +199,13 @@ def is_staticfile(self, path_info: str) -> bool:
197199
else:
198200
static_file = self.files.get(path_info)
199201
return static_file is not None
202+
203+
204+
class GleanApiAccessMiddleware:
205+
def __init__(self, get_response):
206+
self.get_response = get_response
207+
208+
def __call__(self, request):
209+
if request.path.startswith("/api/"):
210+
glean_logger().log_api_accessed(request)
211+
return self.get_response(request)

privaterelay/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@
395395
"waffle.middleware.WaffleMiddleware",
396396
"privaterelay.middleware.AddDetectedCountryToRequestAndResponseHeaders",
397397
"privaterelay.middleware.StoreFirstVisit",
398+
"privaterelay.middleware.GleanApiAccessMiddleware",
398399
]
399400

400401
ROOT_URLCONF = "privaterelay.urls"

privaterelay/tests/glean_tests.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,3 +552,33 @@ def test_log_email_blocked_with_opt_out(
552552

553553
# Check the one glean-server-event log
554554
assert len(caplog.records) == 0
555+
556+
557+
@pytest.mark.django_db
558+
def test_log_api_accessed(
559+
glean_logger: RelayGleanLogger,
560+
caplog: pytest.LogCaptureFixture,
561+
rf: RequestFactory,
562+
) -> None:
563+
"""Check that log_api_accessed emits a Glean server-side log."""
564+
user_agent = "RelayBot/0.9"
565+
path = "/api/v1/profiles/"
566+
request = rf.get(path, HTTP_USER_AGENT=user_agent)
567+
user = make_free_test_user()
568+
request.user = user
569+
570+
glean_logger.log_api_accessed(request)
571+
572+
# Check the one glean-server-event log
573+
assert len(caplog.records) == 1
574+
record = caplog.records[0]
575+
assert_glean_record(record, user_agent=user_agent)
576+
577+
# Check payload structure
578+
payload = json.loads(getattr(record, "payload"))
579+
payload_event = payload["events"][0]
580+
assert payload_event["category"] == "api"
581+
assert payload_event["name"] == "accessed"
582+
assert payload_event["extra"]["endpoint"] == path
583+
assert payload_event["extra"]["method"] == "GET"
584+
assert payload_event["extra"]["fxa_id"] == user.profile.metrics_fxa_id

privaterelay/tests/utils.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ def make_free_test_user(email: str = "") -> User:
2424
user = baker.make(User)
2525
user.profile.server_storage = True
2626
user.profile.save()
27+
uid = str(uuid4())
2728
baker.make(
2829
SocialAccount,
2930
user=user,
30-
uid=str(uuid4()),
31+
uid=uid,
3132
provider="fxa",
32-
extra_data={"avatar": "avatar.png"},
33+
extra_data={"avatar": "avatar.png", "uid": uid},
3334
)
3435
return user
3536

@@ -48,12 +49,17 @@ def upgrade_test_user_to_premium(user: User) -> None:
4849
"""Create an FxA SocialAccount with an unlimited email masks plan."""
4950
if SocialAccount.objects.filter(user=user).exists():
5051
raise Exception("upgrade_test_user_to_premium does not (yet) handle this.")
52+
uid = str(uuid4())
5153
baker.make(
5254
SocialAccount,
5355
user=user,
54-
uid=str(uuid4()),
56+
uid=uid,
5557
provider="fxa",
56-
extra_data={"avatar": "avatar.png", "subscriptions": [premium_subscription()]},
58+
extra_data={
59+
"avatar": "avatar.png",
60+
"subscriptions": [premium_subscription()],
61+
"uid": uid,
62+
},
5763
)
5864

5965

@@ -205,13 +211,20 @@ def get_glean_event(
205211
caplog: pytest.LogCaptureFixture | _LoggingWatcher,
206212
category: str | None = None,
207213
name: str | None = None,
214+
exclude_api_access: bool = True,
208215
) -> dict[str, Any] | None:
209216
"""Return the event payload from a Glean server event log."""
210217
for record in caplog.records:
211218
if record.name == "glean-server-event":
212219
assert hasattr(record, "payload")
213220
event = json.loads(record.payload)["events"][0]
214221
assert isinstance(event, dict)
222+
if (
223+
exclude_api_access
224+
and event["category"] == "api"
225+
and event["name"] == "accessed"
226+
):
227+
continue
215228
if (not category or event["category"] == category) and (
216229
not name or event["name"] == name
217230
):

telemetry/glean/relay-server-metrics.yaml

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ email_mask:
2222
data_reviews:
2323
- https://bugzilla.mozilla.org/show_bug.cgi?id=1882565
2424
notification_emails:
25-
- jwhitlock@mozilla.com
25+
- relay-team@mozilla.com
2626
data_sensitivity:
2727
- interaction
2828
extra_keys: &default_mask_extra_keys
@@ -72,7 +72,7 @@ email_mask:
7272
data_reviews:
7373
- https://bugzilla.mozilla.org/show_bug.cgi?id=1882565
7474
notification_emails:
75-
- jwhitlock@mozilla.com
75+
- relay-team@mozilla.com
7676
data_sensitivity:
7777
- interaction
7878
extra_keys:
@@ -93,7 +93,7 @@ email_mask:
9393
data_reviews:
9494
- https://bugzilla.mozilla.org/show_bug.cgi?id=1882565
9595
notification_emails:
96-
- jwhitlock@mozilla.com
96+
- relay-team@mozilla.com
9797
data_sensitivity:
9898
- interaction
9999
extra_keys: *default_mask_extra_keys
@@ -109,7 +109,7 @@ email:
109109
data_reviews:
110110
- https://bugzilla.mozilla.org/show_bug.cgi?id=1882565
111111
notification_emails:
112-
- jwhitlock@mozilla.com
112+
- relay-team@mozilla.com
113113
data_sensitivity:
114114
- interaction
115115
extra_keys: &default_email_extra_keys
@@ -127,11 +127,35 @@ email:
127127
data_reviews:
128128
- https://bugzilla.mozilla.org/show_bug.cgi?id=1882565
129129
notification_emails:
130-
- jwhitlock@mozilla.com
130+
- relay-team@mozilla.com
131131
data_sensitivity:
132132
- interaction
133133
extra_keys:
134134
<<: *default_email_extra_keys
135135
reason:
136136
description: Code describing why the email was blocked
137137
type: string
138+
139+
api:
140+
accessed:
141+
type: event
142+
description: An API endpoint was accessed.
143+
expires: never
144+
data_reviews:
145+
- https://example.com/data-review
146+
notification_emails:
147+
148+
data_sensitivity:
149+
- interaction
150+
bugs:
151+
- https://mozilla-hub.atlassian.net/browse/MPP-4012
152+
extra_keys:
153+
endpoint:
154+
description: The name of the endpoint accessed
155+
type: string
156+
method:
157+
description: HTTP method used
158+
type: string
159+
fxa_id:
160+
description: Mozilla accounts user ID
161+
type: string

0 commit comments

Comments
 (0)