Skip to content

Commit 5524406

Browse files
committed
#1308 - add IP and request ID to auth logs
1 parent fe262c1 commit 5524406

4 files changed

Lines changed: 74 additions & 1 deletion

File tree

atr/docs/authentication-security.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ ATR also does not ship with built in alert thresholds or automatic blocking rule
256256
An audit log, at `<STATE_DIR>/audit/auth-audit.log`, contains a log of all auth-adjacent operations. This should include authentication success/failures, token issuance/revocation,
257257
and anything else that would be relevant to authentication when investigating a potential security issue.
258258

259+
Each entry is also tagged with the `request_id` and `source_ip` of the originating request, where one is in scope, so auth events can be correlated with the request summaries in `<STATE_DIR>/logs/requests.log`. Both are pulled from the request log context rather than passed at the call site.
260+
259261
This log is logged to by calling the specialised methods on atr.log (all args passed below as kwargs for readability):
260262

261263
```python

atr/log.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,17 @@ def warning(msg: str, **kwargs) -> None:
224224
def _auth_log(**kwargs):
225225
now = datetime.datetime.now(datetime.UTC).isoformat(timespec="milliseconds")
226226
now = now.replace("+00:00", "Z")
227-
kwargs = {"datetime": now, **kwargs}
227+
# Pull the request-scoped correlation fields from the log context, the same
228+
# way the caller's user_id is sourced, so every auth entry can be tied back
229+
# to a request and its origin
230+
context: dict[str, Any] = {"datetime": now}
231+
request_id = get_request_id()
232+
if request_id is not None:
233+
context["request_id"] = request_id
234+
source_ip = get_context("source_ip")
235+
if isinstance(source_ip, str):
236+
context["source_ip"] = source_ip
237+
kwargs = {**context, **kwargs}
228238
msg = json.dumps(kwargs, allow_nan=False)
229239
logger = logging.getLogger("atr.auth")
230240
logger.info(msg)

atr/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,7 @@ async def handle_rate_limit(e):
11791179
async def _reset_request_log_context():
11801180
log.clear_context()
11811181
log.add_context(request_id=str(uuid.uuid4()))
1182+
log.add_context(source_ip=quart.request.remote_addr)
11821183
session = await sessions.read()
11831184
if isinstance(session, sql.UserSession):
11841185
log.add_context(user_id=session.uid)

tests/unit/test_auth_audit_log.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
import json
18+
import logging
19+
20+
import atr.log as log
21+
22+
23+
def _capture_auth_log() -> list[str]:
24+
captured: list[str] = []
25+
26+
class _Handler(logging.Handler):
27+
def emit(self, record: logging.LogRecord) -> None:
28+
captured.append(record.getMessage())
29+
30+
logger = logging.getLogger("atr.auth")
31+
logger.handlers.clear()
32+
logger.addHandler(_Handler())
33+
logger.setLevel(logging.INFO)
34+
logger.propagate = False
35+
return captured
36+
37+
38+
def test_auth_log_omits_request_fields_when_no_request_context_is_bound() -> None:
39+
log.clear_context()
40+
captured = _capture_auth_log()
41+
42+
log.auth_success(type="oauth", asfuid="alice")
43+
44+
entry = json.loads(captured[-1])
45+
assert "request_id" not in entry
46+
assert "source_ip" not in entry
47+
48+
49+
def test_auth_log_carries_request_id_and_source_ip_from_the_log_context() -> None:
50+
log.clear_context()
51+
log.add_context(request_id="req-123", source_ip="203.0.113.7")
52+
captured = _capture_auth_log()
53+
try:
54+
log.auth_failure(type="jwt", reason="bad token", asfuid="bob")
55+
56+
entry = json.loads(captured[-1])
57+
assert entry["request_id"] == "req-123"
58+
assert entry["source_ip"] == "203.0.113.7"
59+
finally:
60+
log.clear_context()

0 commit comments

Comments
 (0)