From 50d1b5b06d470dcfe510b92061f4b3a5435410dc Mon Sep 17 00:00:00 2001 From: fabi Date: Wed, 29 Jul 2026 21:37:39 +0200 Subject: [PATCH] fix(admin): reject quota_tolerance = 0 instead of silently blocking every upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero is inside the documented 0–1 range and catastrophic. The per-user limit is `free_disk * tolerance / active_uploaders`, so a tolerance of 0 makes every limit 0 and refuses EVERY upload -- mid-event, with "Du hast dein Upload-Limit für dieses Event erreicht", an error naming the wrong cause entirely. An admin reaching for an off-switch wants `storage_quota_enabled`; the rejection now says so. Rejecting the value rather than raising the floor. A floor of 0.01 was the obvious fix and it is wrong: very small tolerances are legitimate -- they are how a large disk is throttled down to a sensible per-guest ceiling, and how the quota specs steer it (tolerance = target * active / free lands around 1e-5 on the 174 GB volume this suite runs on). A floor would forbid real configurations, and would have broken the entire storage-quota describe block, to prevent one typo. Verified: those four tests still pass. Tests: the rejection, that the stored value is untouched (validation fully precedes any write), and the mirror -- 0.00001 still round-trips -- so the guard can't quietly become a floor later. Co-Authored-By: Claude Opus 5 --- backend/src/handlers/admin.rs | 17 ++++++++++++++++ e2e/specs/05-admin/config.spec.ts | 34 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/backend/src/handlers/admin.rs b/backend/src/handlers/admin.rs index d001c4d..3233268 100644 --- a/backend/src/handlers/admin.rs +++ b/backend/src/handlers/admin.rs @@ -197,6 +197,23 @@ pub async fn patch_config( "Wert für {key} liegt außerhalb des zulässigen Bereichs ({min}–{max})." ))); } + // Zero is in range and catastrophic. `quota_tolerance` is the multiplier in + // `free_disk * tolerance / active_uploaders`, so 0 makes every per-user limit 0 and + // refuses EVERY upload — mid-event, with "Du hast dein Upload-Limit für dieses Event + // erreicht", an error naming the wrong cause entirely. `storage_quota_enabled` is the + // intended off-switch. + // + // Rejecting the value rather than raising the floor: very small tolerances are + // legitimate (they are how a large disk is throttled down to a sensible per-guest + // ceiling, and how the e2e quota tests steer it — around 1e-5 on a 174 GB volume), so + // a floor of, say, 0.01 would forbid real configurations to prevent one typo. + if key_str == "quota_tolerance" && n == 0.0 { + return Err(AppError::BadRequest( + "quota_tolerance = 0 würde jeden Upload blockieren. Zum Abschalten der \ + Speicher-Quote stattdessen „Speicher-Quote aktiv“ ausschalten." + .into(), + )); + } } else if BOOL_KEYS.contains(&key_str) { match value.trim().to_ascii_lowercase().as_str() { "true" | "false" | "1" | "0" | "yes" | "no" | "on" | "off" => {} diff --git a/e2e/specs/05-admin/config.spec.ts b/e2e/specs/05-admin/config.spec.ts index 4119c7e..dca1005 100644 --- a/e2e/specs/05-admin/config.spec.ts +++ b/e2e/specs/05-admin/config.spec.ts @@ -68,6 +68,40 @@ test.describe('Admin — config API', () => { expect(cfg.privacy_note).toBe(note); await api.patchConfig(adminToken, { privacy_note: '' }); }); + test('quota_tolerance = 0 is rejected, with a pointer to the real off-switch', async ({ + api, + adminToken, + }) => { + // Zero is inside the documented 0–1 range and catastrophic: the per-user limit is + // `free_disk * tolerance / active_uploaders`, so 0 refuses EVERY upload — mid-event, with + // "Du hast dein Upload-Limit für dieses Event erreicht", which names the wrong cause + // entirely. `storage_quota_enabled` is what an admin reaching for an off-switch wants. + const res = await fetch( + (process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/admin/config', + { + method: 'PATCH', + headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ quota_tolerance: '0' }), + } + ); + expect(res.status).toBe(400); + expect( + (await res.text()).toLowerCase(), + 'the error must name the switch the admin actually wanted' + ).toContain('speicher-quote'); + + // The value is untouched — validation fully precedes any write. + expect((await api.getConfig(adminToken)).quota_tolerance).toBe('0.75'); + }); + + test('a very small quota_tolerance is still accepted', async ({ api, adminToken }) => { + // The mirror. Rejecting 0 must not become a floor: small tolerances are how a large disk is + // throttled to a sensible per-guest ceiling, and how the quota specs steer it (~1e-5 on a + // 174 GB volume). A floor of 0.01 would forbid real configurations to prevent one typo. + await api.patchConfig(adminToken, { quota_tolerance: '0.00001' }); + expect((await api.getConfig(adminToken)).quota_tolerance).toBe('0.00001'); + await api.patchConfig(adminToken, { quota_tolerance: '0.75' }); + }); }); test.describe('Admin — stats', () => {