Skip to content

Commit 2b8583f

Browse files
committed
Move audit logging demo out of test.py into example/audit_logging.py
The previous commit accidentally bundled the audit-logging configuration walkthrough — and a local RBAC email change — into the test.py playground script. test.py is meant to be a personal sandbox kept out of releases. Pull the audit demo back into a dedicated example module (example/audit_logging.py) so users still get a copy-pasteable StreamHandler + formatter + redact-filter block, and restore test.py to the version that was already tracked in the tree before d2ef0e7.
1 parent e2e7944 commit 2b8583f

2 files changed

Lines changed: 106 additions & 50 deletions

File tree

example/audit_logging.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Stand-alone audit logging example for flask-s3-viewer.
2+
3+
Copy the ``configure_audit_logger`` call into your own app's startup
4+
to start auditing. The block is intentionally self-contained — no
5+
other example file imports from it.
6+
7+
The ``flask_s3_viewer.audit`` logger emits one record per S3 CRUD
8+
action (list / download / upload / delete / presign). Every record
9+
carries these LogRecord extras:
10+
11+
action / namespace / key / user / result / status_code /
12+
client_ip / user_agent / request_id (+ error on failure)
13+
14+
Multi-file requests emit one row per file. All rows from the same
15+
Flask request share one ``request_id`` (8 hex chars), so plaintext
16+
greps can group them, e.g. ``grep "req=a1b2c3d4" audit.log``.
17+
18+
Drop this file's body into your app's startup (after ``logging.basicConfig``
19+
or whatever root logging your host already does) and adjust the
20+
handler / formatter to taste — ``FileHandler`` with rotation, a JSON
21+
formatter via ``python-json-logger``, a Loki/Fluent Bit sink, etc.
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import logging
27+
28+
29+
class DemoUserRedactFilter(logging.Filter):
30+
"""Cheap email-tail redaction for the demo.
31+
32+
Production deployments should swap this out for the redaction
33+
policy that matches their compliance requirements — see
34+
``docs/source/usage/configuration.rst`` for a ``RedactFilter`` /
35+
``KeyErrorRedactFilter`` reference set.
36+
"""
37+
38+
def filter(self, record: logging.LogRecord) -> bool:
39+
user = getattr(record, "user", None)
40+
if user and "@" in user:
41+
local, domain = user.split("@", 1)
42+
record.user = f"{local[:2]}***@{domain}"
43+
return True
44+
45+
46+
def configure_audit_logger(
47+
*,
48+
level: int = logging.INFO,
49+
propagate: bool = True,
50+
redact_user_emails: bool = True,
51+
) -> logging.Logger:
52+
"""Attach a ``StreamHandler`` to ``flask_s3_viewer.audit`` and return it.
53+
54+
Parameters
55+
----------
56+
level
57+
Audit logger level. ``logging.INFO`` is the recommended default;
58+
denied (401/403) records emit at ``WARNING`` and exceptions at
59+
``ERROR`` so they survive higher thresholds too.
60+
propagate
61+
When ``True`` (default) audit records also reach the host's root
62+
logger. Set ``False`` if you want the audit stream completely
63+
isolated from your application logs.
64+
redact_user_emails
65+
Install :class:`DemoUserRedactFilter` so emails appear as
66+
``jo***@example.com`` in the demo output. Turn off (or replace
67+
with your own filter) for production.
68+
"""
69+
audit_logger = logging.getLogger("flask_s3_viewer.audit")
70+
audit_logger.setLevel(level)
71+
audit_logger.propagate = propagate
72+
73+
handler = logging.StreamHandler()
74+
handler.setFormatter(
75+
logging.Formatter(
76+
"AUDIT %(asctime)s %(levelname)s "
77+
"action=%(action)s namespace=%(namespace)s "
78+
"key=%(key)s user=%(user)s result=%(result)s "
79+
"status=%(status_code)s req=%(request_id)s "
80+
"ip=%(client_ip)s",
81+
),
82+
)
83+
audit_logger.addHandler(handler)
84+
85+
if redact_user_emails:
86+
audit_logger.addFilter(DemoUserRedactFilter())
87+
88+
return audit_logger
89+
90+
91+
if __name__ == "__main__":
92+
# Quick smoke test — emits one anonymous record outside a Flask
93+
# request context. Run with: python -m example.audit_logging
94+
from flask_s3_viewer.audit import emit
95+
from flask_s3_viewer.auth import ACTION_LIST
96+
97+
configure_audit_logger()
98+
emit(
99+
action=ACTION_LIST,
100+
namespace="demo",
101+
key="demo/key",
102+
user="alice@example.com",
103+
result="ok",
104+
status_code=200,
105+
)

test.py

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,55 +8,6 @@
88

99
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(asctime)s: %(message)s")
1010

11-
# ---------------------------------------------------------------------------
12-
# Audit logger setup (flask_s3_viewer.audit)
13-
#
14-
# Independent of the host root logger above. The audit logger emits one
15-
# record per S3 CRUD action (list/download/upload/delete/presign) with
16-
# extra fields:
17-
# action / namespace / key / user / result / status_code /
18-
# client_ip / user_agent / request_id (+ error on failure)
19-
#
20-
# Multi-file uploads emit one row per file; every row in the same Flask
21-
# request shares one `request_id` (8 hex chars) so plain-text grep can
22-
# group them, e.g. `grep "req=a1b2c3d4" audit.log`.
23-
#
24-
# Copy this block into your own app to start auditing — adjust the
25-
# handler (StreamHandler / FileHandler / structured JSON via
26-
# python-json-logger) and the formatter to fit your log pipeline.
27-
# ---------------------------------------------------------------------------
28-
audit_logger = logging.getLogger("flask_s3_viewer.audit")
29-
audit_handler = logging.StreamHandler()
30-
audit_handler.setFormatter(logging.Formatter(
31-
"AUDIT %(asctime)s %(levelname)s "
32-
"action=%(action)s namespace=%(namespace)s "
33-
"key=%(key)s user=%(user)s result=%(result)s "
34-
"status=%(status_code)s req=%(request_id)s "
35-
"ip=%(client_ip)s"
36-
))
37-
audit_logger.addHandler(audit_handler)
38-
audit_logger.setLevel(logging.INFO)
39-
# Set False if you do NOT want audit lines also reaching the root
40-
# handler installed by logging.basicConfig above. Left True here so
41-
# the demo shows both streams side by side.
42-
# audit_logger.propagate = False
43-
44-
45-
# Optional: demo-only PII-soft user field. Production policies should
46-
# replace this with the redaction filter that matches your compliance
47-
# requirements (see docs/source/usage/configuration.rst).
48-
class _DemoUserRedactFilter(logging.Filter):
49-
def filter(self, record: logging.LogRecord) -> bool:
50-
user = getattr(record, "user", None)
51-
if user and "@" in user:
52-
local, domain = user.split("@", 1)
53-
record.user = f"{local[:2]}***@{domain}"
54-
return True
55-
56-
57-
# Comment the next line out to see raw email addresses in the demo log.
58-
audit_logger.addFilter(_DemoUserRedactFilter())
59-
6011
app = Flask(__name__)
6112

6213
# For test, disable template caching
@@ -108,7 +59,7 @@ def filter(self, record: logging.LogRecord) -> bool:
10859
# ---------------------------------------------------------------------------
10960
RBAC_POLICY: dict[str, dict[str, dict[str, set[str]]]] = {
11061
# Replace these with your real Google account emails.
111-
"joseph.jeong@kakaopiccoma.com": {
62+
"test@test.com": {
11263
"flask-s3-viewer": {
11364
"list": {""},
11465
"upload": {"test/aaaa/"},

0 commit comments

Comments
 (0)