fix(e12): close the eighteen findings the E12 review round left standing
The deep review of the branch raised 47 findings, 18 of which survived adversarial verification. Grouped by what they actually were: **The size ceiling inverted at its boundary.** `remaining_bytes` clamped to 0 for a user already at their storage cap, and `build_ydl_opts` reads 0 as "no limit" — so the one user over quota was the only one downloading with no ceiling at all. `check_enqueue` could not catch it: it ran while the job was queued and the user was still under the cap. The arithmetic moves into `quota.remaining_bytes` (where every other max_bytes decision lives, and where it can be tested), and the worker now fails the job outright rather than proceeding unbounded — as a non-retryable `quota_exceeded`, since retrying cannot help until space is freed. **Silent failures in the new account panel.** All three actions went through `quiet: true` endpoints, which suppress the global error dialog on the promise that the caller reports inline — with no `onError`, that promise was broken and a refusal rendered as nothing. On the broken-SMTP instance this panel exists for, an admin would read silence as "the mail went out". The co-invalidation set and the email-off predicate move to `lib/adminUsers`, shared with the pending-access queue, so the two copies of these mutations cannot drift apart again — which is how half of this was lost in the first place. **A rollback recipe that destroyed data without working.** `alembic downgrade -1` steps back one migration, but a release routinely ships several (0.80.0 shipped four): it drops the newest one's columns and leaves the schema at a revision the older image also cannot resolve, i.e. the crash loop it was meant to escape, minus the data. The dump restore is now the recommended path, and the alembic route asks the target version for its own head revision instead of counting backwards. Also: redirects named in the urlguard docstring beside the DNS-rebinding residual they share a fix with (SEC-17); a malformed port refused as `bad_port` instead of escaping as a 500; the pull cooldown extended to the sibling route spending the same QuotaAction; restore stops the app before dropping its tables; the PowerShell cleanup can no longer decide the script's verdict on 5.1, nor report the wrong exit code; the documented one-liners hardened to match the scripts; the registry's retention window stated where pinning is advertised; the two banners made mutually exclusive so neither can shadow the other; `hover:bg-hover` (a utility this project never generated) replaced by the `selectable` recipe; the clipboard helper reused; and the sync cooldown's copy swept to the two callers that discarded it. Verified: gate green (vitest 678, pytest 949), and the panel, the row hover and the reset banner UAT'd on localdev.
This commit is contained in:
@@ -440,13 +440,19 @@ def _download(
|
||||
cookiefile = credentials.resolve_cookiefile(
|
||||
cdb, user_id, url, asset_sig, spec, staging
|
||||
)
|
||||
# SEC-13: what this user has left. `check_enqueue` only compares the CURRENT footprint
|
||||
# against the cap — a download's size is unknown until it finishes, so a single job could
|
||||
# previously blow through the cap by any amount. Handing the remainder to yt-dlp as
|
||||
# `max_filesize` makes the format selector refuse an oversized file up front instead.
|
||||
lim = quota.resolve(cdb, user_id)
|
||||
if not lim.unlimited:
|
||||
remaining_bytes = max(0, lim.max_bytes - quota.footprint(cdb, user_id))
|
||||
# SEC-13: what this user has left. `check_enqueue` only compared the footprint as it
|
||||
# stood when the job was QUEUED — a download's size is unknown until it finishes, so a
|
||||
# single job could previously blow through the cap by any amount. Handing the remainder
|
||||
# to yt-dlp as `max_filesize` makes the format selector refuse an oversized file up
|
||||
# front instead.
|
||||
remaining_bytes = quota.remaining_bytes(cdb, user_id)
|
||||
# …and zero is a REFUSAL, not "no limit". Earlier jobs of this user may have filled the cap
|
||||
# while this one sat in the queue, and 0 is not an expressible yt-dlp `max_filesize`, so
|
||||
# passing it down would leave the ceiling unset — the one user already at their cap would be
|
||||
# the only one downloading unbounded. Fail the job here instead. Not retryable until they
|
||||
# free space, which is what the `quota_exceeded` category tells them.
|
||||
if remaining_bytes == 0:
|
||||
raise RuntimeError(dlerrors.QUOTA_FULL_MESSAGE)
|
||||
info = _media_info(
|
||||
_extract_with_fallback(
|
||||
job_id,
|
||||
@@ -551,14 +557,8 @@ def _download(
|
||||
except Exception as exc: # noqa: BLE001 — surface any failure to the user, keep worker alive
|
||||
msg = str(exc)[:500]
|
||||
code = dlerrors.classify(msg)
|
||||
# SEC-12: for a user-supplied URL, a transport-level failure text distinguishes "connection
|
||||
# refused" from "HTTP Error 401" from "timed out" per host:port — which is a port scanner
|
||||
# with extra steps, since the raw text is shown to the user behind the details disclosure.
|
||||
# The urlguard above stops it aiming at our own network; this stops the remaining readout.
|
||||
# Only the network class, and only for `url` jobs: a YouTube job has no user-chosen host, and
|
||||
# every other class (login_required, geo_blocked, drm, …) is real, useful diagnosis.
|
||||
if source_kind == "url" and code == "network":
|
||||
msg = "The server could not reach that address."
|
||||
# SEC-12: redact a transport-level readout for user-supplied URLs (see user_error_text).
|
||||
msg = dlerrors.user_error_text(source_kind, msg, code)
|
||||
with SessionLocal() as db:
|
||||
asset = db.get(MediaAsset, asset_id)
|
||||
if asset:
|
||||
|
||||
Reference in New Issue
Block a user