Skip to content

Commit 18faa17

Browse files
committed
Surface bucket in audit records and stop blocking the presign Upload button
Audit lines now carry a bucket field next to namespace — both as a LogRecord extra and as a bucket=<name> token sitting right after namespace= in the message body. The blueprint's url_value_preprocessor parks the active viewer's bucket name on g.FSV_AUDIT_BUCKET and emit() picks it up automatically, so the value rides alongside request_id for free. Out-of-request emits fall back to an empty bucket so JSON pipelines see a consistent shape. The sanitiser and length cap apply to the new field the same way they do to namespace. The presign upload form no longer blocks the Upload button on a chain of HEAD + sign round-trips. Selecting files just renders the chips and the Upload button immediately; the presign batch is fetched on click with a brief "Preparing…" spinner and a re-click guard. The core JS keeps the same public surface — only _upload_form.html changed — and the default (multipart) upload path is untouched. Tests grow a TestBucketField suite and a multi-namespace assertion; docs note the new field, the message-token order, and the new presign-mode UX. The change is grouped under the existing 1.1.0 unreleased block in CHANGELOG.
1 parent 2b8583f commit 18faa17

14 files changed

Lines changed: 281 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
3939
`req=<id>` token appended to the human-readable message body.
4040
Outside a request context every `emit()` gets a fresh id. The
4141
`emit()` public signature is unchanged.
42+
- Audit records now include a `bucket` field (record extra +
43+
`bucket=<name>` message token) reflecting the viewer's S3 bucket
44+
name. The blueprint's `url_value_preprocessor` resolves the bucket
45+
once per request so every audit row carries it without any
46+
per-view bookkeeping; emits from outside a request context yield an
47+
empty bucket unless the caller pre-sets `g.FSV_AUDIT_BUCKET`. The
48+
`emit()` public signature is unchanged.
4249

4350
**Changed**
4451

@@ -62,6 +69,18 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6269
`file_list` presign, invalid prefix, denied auth) still emit a
6370
single prefix-keyed row. The `emit()` public signature is unchanged;
6471
only the per-request emit count grew (1 → N).
72+
- `upload_type='presign'` mode no longer pre-issues conflict-check
73+
presign URLs at file-selection time. The previous flow walked every
74+
selected file through a serial `post_presign` round-trip before any
75+
chip rendered, so picking N files incurred N sequential S3 / signer
76+
hops before the user could even see what they had staged. Chips and
77+
the Upload button now appear immediately after the file picker
78+
resolves; presign URL issuance is deferred to the Upload click.
79+
Conflict (HTTP 409) and permission-denied (HTTP 403) dialogs
80+
therefore surface after Upload instead of after file selection.
81+
`default` upload mode is unaffected. `core.js` public surface
82+
(`readyFileHandling`, `uploadFiles`, `preventDefaults`) is unchanged
83+
— only `_upload_form.html` was touched.
6584

6685
**Fixed**
6786

docs/doctrees/changelog.doctree

2.09 KB
Binary file not shown.

docs/doctrees/environment.pickle

0 Bytes
Binary file not shown.
1.89 KB
Binary file not shown.

docs/html/_sources/usage/configuration.rst.txt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ exceptions emit at ``ERROR``.
438438
- ``action`` — one of ``list``, ``download``, ``upload``, ``delete``,
439439
``presign``
440440
- ``namespace`` — viewer namespace the request landed on
441+
- ``bucket`` — S3 bucket name resolved from the viewer config for the
442+
current request. Populated automatically by the blueprint's
443+
``url_value_preprocessor``; the empty string when ``emit()`` is
444+
called outside a Flask request context unless the caller pre-sets
445+
``g.FSV_AUDIT_BUCKET`` themselves.
441446
- ``key`` — canonical S3 key / prefix (post-``base_path``)
442447
- ``user`` — authenticated email or the literal string ``anonymous``
443448
- ``result`` — ``ok`` / ``denied`` / ``error``
@@ -458,8 +463,9 @@ The human-readable message is a single space-separated key=value line:
458463

459464
.. code-block:: text
460465
461-
action=download namespace=fsv-test key=docs/report.pdf
462-
user=alice@example.com result=ok status=200 req=a1b2c3d4
466+
action=download namespace=fsv-test bucket=fsv-bucket
467+
key=docs/report.pdf user=alice@example.com result=ok status=200
468+
req=a1b2c3d4
463469
464470
Newlines, carriage returns, and other ASCII control bytes inside
465471
attacker-controllable fields (key, email, User-Agent, exception
@@ -510,7 +516,7 @@ one request produce one row, not two.
510516
audit_handler = logging.FileHandler('/var/log/flask_s3_viewer/audit.jsonl')
511517
audit_handler.setFormatter(jsonlogger.JsonFormatter(
512518
'%(asctime)s %(levelname)s %(action)s %(namespace)s '
513-
'%(key)s %(user)s %(result)s %(status_code)s '
519+
'%(bucket)s %(key)s %(user)s %(result)s %(status_code)s '
514520
'%(client_ip)s %(user_agent)s'
515521
))
516522
audit = logging.getLogger('flask_s3_viewer.audit')

docs/html/changelog.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ <h2>[1.1.0] - unreleased<a class="headerlink" href="#unreleased" title="Link to
145145
<code class="docutils literal notranslate"><span class="pre">req=&lt;id&gt;</span></code> token appended to the human-readable message body.
146146
Outside a request context every <code class="docutils literal notranslate"><span class="pre">emit()</span></code> gets a fresh id. The
147147
<code class="docutils literal notranslate"><span class="pre">emit()</span></code> public signature is unchanged.</p></li>
148+
<li><p>Audit records now include a <code class="docutils literal notranslate"><span class="pre">bucket</span></code> field (record extra +
149+
<code class="docutils literal notranslate"><span class="pre">bucket=&lt;name&gt;</span></code> message token) reflecting the viewer’s S3 bucket
150+
name. The blueprint’s <code class="docutils literal notranslate"><span class="pre">url_value_preprocessor</span></code> resolves the bucket
151+
once per request so every audit row carries it without any
152+
per-view bookkeeping; emits from outside a request context yield an
153+
empty bucket unless the caller pre-sets <code class="docutils literal notranslate"><span class="pre">g.FSV_AUDIT_BUCKET</span></code>. The
154+
<code class="docutils literal notranslate"><span class="pre">emit()</span></code> public signature is unchanged.</p></li>
148155
</ul>
149156
<p><strong>Changed</strong></p>
150157
<ul class="simple">

docs/html/searchindex.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/html/usage/configuration.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,11 @@ <h2>Audit logging<a class="headerlink" href="#audit-logging" title="Link to this
470470
<li><p><code class="docutils literal notranslate"><span class="pre">action</span></code> — one of <code class="docutils literal notranslate"><span class="pre">list</span></code>, <code class="docutils literal notranslate"><span class="pre">download</span></code>, <code class="docutils literal notranslate"><span class="pre">upload</span></code>, <code class="docutils literal notranslate"><span class="pre">delete</span></code>,
471471
<code class="docutils literal notranslate"><span class="pre">presign</span></code></p></li>
472472
<li><p><code class="docutils literal notranslate"><span class="pre">namespace</span></code> — viewer namespace the request landed on</p></li>
473+
<li><p><code class="docutils literal notranslate"><span class="pre">bucket</span></code> — S3 bucket name resolved from the viewer config for the
474+
current request. Populated automatically by the blueprint’s
475+
<code class="docutils literal notranslate"><span class="pre">url_value_preprocessor</span></code>; the empty string when <code class="docutils literal notranslate"><span class="pre">emit()</span></code> is
476+
called outside a Flask request context unless the caller pre-sets
477+
<code class="docutils literal notranslate"><span class="pre">g.FSV_AUDIT_BUCKET</span></code> themselves.</p></li>
473478
<li><p><code class="docutils literal notranslate"><span class="pre">key</span></code> — canonical S3 key / prefix (post-<code class="docutils literal notranslate"><span class="pre">base_path</span></code>)</p></li>
474479
<li><p><code class="docutils literal notranslate"><span class="pre">user</span></code> — authenticated email or the literal string <code class="docutils literal notranslate"><span class="pre">anonymous</span></code></p></li>
475480
<li><p><code class="docutils literal notranslate"><span class="pre">result</span></code><code class="docutils literal notranslate"><span class="pre">ok</span></code> / <code class="docutils literal notranslate"><span class="pre">denied</span></code> / <code class="docutils literal notranslate"><span class="pre">error</span></code></p></li>
@@ -488,8 +493,9 @@ <h2>Audit logging<a class="headerlink" href="#audit-logging" title="Link to this
488493
</ul>
489494
</div></blockquote>
490495
<p>The human-readable message is a single space-separated key=value line:</p>
491-
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>action=download namespace=fsv-test key=docs/report.pdf
492-
user=alice@example.com result=ok status=200 req=a1b2c3d4
496+
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>action=download namespace=fsv-test bucket=fsv-bucket
497+
key=docs/report.pdf user=alice@example.com result=ok status=200
498+
req=a1b2c3d4
493499
</pre></div>
494500
</div>
495501
<p>Newlines, carriage returns, and other ASCII control bytes inside
@@ -531,7 +537,7 @@ <h2>Audit logging<a class="headerlink" href="#audit-logging" title="Link to this
531537
<span class="linenos"> 4</span><span class="n">audit_handler</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">FileHandler</span><span class="p">(</span><span class="s1">&#39;/var/log/flask_s3_viewer/audit.jsonl&#39;</span><span class="p">)</span>
532538
<span class="linenos"> 5</span><span class="n">audit_handler</span><span class="o">.</span><span class="n">setFormatter</span><span class="p">(</span><span class="n">jsonlogger</span><span class="o">.</span><span class="n">JsonFormatter</span><span class="p">(</span>
533539
<span class="linenos"> 6</span> <span class="s1">&#39;</span><span class="si">%(asctime)s</span><span class="s1"> </span><span class="si">%(levelname)s</span><span class="s1"> </span><span class="si">%(action)s</span><span class="s1"> </span><span class="si">%(namespace)s</span><span class="s1"> &#39;</span>
534-
<span class="linenos"> 7</span> <span class="s1">&#39;</span><span class="si">%(key)s</span><span class="s1"> </span><span class="si">%(user)s</span><span class="s1"> </span><span class="si">%(result)s</span><span class="s1"> </span><span class="si">%(status_code)s</span><span class="s1"> &#39;</span>
540+
<span class="linenos"> 7</span> <span class="s1">&#39;</span><span class="si">%(bucket)s</span><span class="s1"> </span><span class="si">%(key)s</span><span class="s1"> </span><span class="si">%(user)s</span><span class="s1"> </span><span class="si">%(result)s</span><span class="s1"> </span><span class="si">%(status_code)s</span><span class="s1"> &#39;</span>
535541
<span class="linenos"> 8</span> <span class="s1">&#39;</span><span class="si">%(client_ip)s</span><span class="s1"> </span><span class="si">%(user_agent)s</span><span class="s1">&#39;</span>
536542
<span class="linenos"> 9</span><span class="p">))</span>
537543
<span class="linenos">10</span><span class="n">audit</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s1">&#39;flask_s3_viewer.audit&#39;</span><span class="p">)</span>

docs/source/usage/configuration.rst

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ exceptions emit at ``ERROR``.
438438
- ``action`` — one of ``list``, ``download``, ``upload``, ``delete``,
439439
``presign``
440440
- ``namespace`` — viewer namespace the request landed on
441+
- ``bucket`` — S3 bucket name resolved from the viewer config for the
442+
current request. Populated automatically by the blueprint's
443+
``url_value_preprocessor``; the empty string when ``emit()`` is
444+
called outside a Flask request context unless the caller pre-sets
445+
``g.FSV_AUDIT_BUCKET`` themselves.
441446
- ``key`` — canonical S3 key / prefix (post-``base_path``)
442447
- ``user`` — authenticated email or the literal string ``anonymous``
443448
- ``result`` — ``ok`` / ``denied`` / ``error``
@@ -458,8 +463,9 @@ The human-readable message is a single space-separated key=value line:
458463

459464
.. code-block:: text
460465
461-
action=download namespace=fsv-test key=docs/report.pdf
462-
user=alice@example.com result=ok status=200 req=a1b2c3d4
466+
action=download namespace=fsv-test bucket=fsv-bucket
467+
key=docs/report.pdf user=alice@example.com result=ok status=200
468+
req=a1b2c3d4
463469
464470
Newlines, carriage returns, and other ASCII control bytes inside
465471
attacker-controllable fields (key, email, User-Agent, exception
@@ -510,7 +516,7 @@ one request produce one row, not two.
510516
audit_handler = logging.FileHandler('/var/log/flask_s3_viewer/audit.jsonl')
511517
audit_handler.setFormatter(jsonlogger.JsonFormatter(
512518
'%(asctime)s %(levelname)s %(action)s %(namespace)s '
513-
'%(key)s %(user)s %(result)s %(status_code)s '
519+
'%(bucket)s %(key)s %(user)s %(result)s %(status_code)s '
514520
'%(client_ip)s %(user_agent)s'
515521
))
516522
audit = logging.getLogger('flask_s3_viewer.audit')

flask_s3_viewer/audit.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
2121
- ``action`` — one of ``list``/``download``/``upload``/``delete``/``presign``
2222
- ``namespace`` — the viewer namespace this request landed on
23+
- ``bucket`` — S3 bucket name resolved from the viewer config for the
24+
current request (``''`` outside a request context unless the caller
25+
pre-set ``g.FSV_AUDIT_BUCKET``)
2326
- ``key`` — the canonical S3 key / prefix touched (``''`` for listings
2427
that operate on the namespace root)
2528
- ``user`` — authenticated email or ``"anonymous"``
@@ -141,12 +144,20 @@ def emit(
141144

142145
client_ip = ''
143146
user_agent = ''
147+
bucket_raw = ''
144148
if has_request_context():
145149
client_ip = _sanitize(request.remote_addr, limit=64)
146150
user_agent = _sanitize(
147151
request.headers.get('User-Agent', ''),
148152
limit=MAX_UA_LEN,
149153
)
154+
# ``g.FSV_AUDIT_BUCKET`` is set by the blueprint's
155+
# ``url_value_preprocessor`` once per request so every audit row
156+
# in this request carries the resolved viewer's S3 bucket name
157+
# without each view having to set it. Host code emitting from
158+
# outside the blueprint may pre-set ``g.FSV_AUDIT_BUCKET``
159+
# themselves; otherwise it falls back to the empty string.
160+
bucket_raw = getattr(g, 'FSV_AUDIT_BUCKET', '') or ''
150161
# Lazy-init a per-request id on flask.g so every emit inside one
151162
# request shares the same correlation token. The key name is an
152163
# internal sentinel — host code should consume the id via
@@ -161,6 +172,8 @@ def emit(
161172
# every call gets its own id.
162173
request_id = secrets.token_hex(4)
163174

175+
safe_bucket = _sanitize(bucket_raw, limit=_MAX_FIELD_LEN)
176+
164177
error_msg = ''
165178
if exc is not None:
166179
error_msg = _sanitize(
@@ -171,6 +184,7 @@ def emit(
171184
extra: dict[str, Any] = {
172185
'action': safe_action,
173186
'namespace': safe_namespace,
187+
'bucket': safe_bucket,
174188
'key': safe_key,
175189
'user': safe_user,
176190
'result': safe_result,
@@ -184,7 +198,7 @@ def emit(
184198

185199
message = (
186200
f'action={safe_action} namespace={safe_namespace} '
187-
f'key={safe_key} user={safe_user} '
201+
f'bucket={safe_bucket} key={safe_key} user={safe_user} '
188202
f'result={safe_result} status={extra["status_code"]} '
189203
f'req={request_id}'
190204
)

0 commit comments

Comments
 (0)