From 06285a3a53685c181d9da0c61262b247c9215dfd Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Wed, 26 Aug 2026 13:57:17 +0200 Subject: [PATCH] Ein Token darf jetzt mehrere Ordner -- statt einem oder allen Das Fotostudio schreibt nach vehicles/ UND items/, der Token war aber auf "vehicles" gefesselt. Damit blieb nur die Wahl zwischen "einer davon" und "darf ueberall": das eine lehnt die Haelfte der Uploads mit 403 ab, das andere erlaubt auch sicherungen/. Gemessen und dabei aufgefallen: config.upload.lua setzt kein clothingPath, also gilt die Vorgabe items/{model}.png -- und die durfte dieser Token nicht. Kleidungs-Uploads aus dem Studio sind also stillschweigend an 403 gescheitert. Die Spalte bleibt ein Textfeld, mehrere Werte stehen durch Komma darin. Damit gilt JEDER bestehende Token unveraendert weiter: ein Wert ohne Komma ist eine Liste mit einem Eintrag. Kein Migrationsschritt, kein Sonderfall. Geprueft mit acht Faellen, darunter zwei, die scheitern MUESSEN: items/ unter der Fessel "vehicles" (403) und "vehiclesXYZ/a.png" unter "vehicles" -- das angehaengte / war schon vorher da und bleibt, sonst wuerde ein Praefix seinen eigenen Namensvetter durchlassen. Die 403-Meldung nennt jetzt alle erlaubten Ordner. Vorher haette sie bei mehreren Fesseln "vehicles,items/" geschrieben -- einen Ordner, den es nicht gibt. Wer abgewiesen wird, soll lesen koennen, wohin er darf. Co-Authored-By: Claude Opus 5 --- server/src/auth.ts | 40 ++++++++++++++++++++++++++++++--- server/src/routes/dash.ts | 26 +++++++++++++++++++-- server/src/routes/fivemanage.ts | 4 ++-- server/src/routes/upload.ts | 4 ++-- server/ui/src/pages/Tokens.tsx | 15 ++++++++----- 5 files changed, 75 insertions(+), 14 deletions(-) diff --git a/server/src/auth.ts b/server/src/auth.ts index 107317d..63dcb28 100644 --- a/server/src/auth.ts +++ b/server/src/auth.ts @@ -147,10 +147,44 @@ export function tokenFromHeader(header: string | undefined): Token | null { } /** Darf dieser Token auf diesen Pfad schreiben? */ +/** + * Die Fesseln eines Tokens als Liste. + * + * MEHRERE STATT EINER, seit dem 23.08.2026. Das Fotostudio schreibt nach + * `vehicles/` UND `items/`, und mit einer einzigen Fessel blieb nur die Wahl + * zwischen "einer davon" und "darf ueberall". Beides war falsch: das eine + * lehnt die Haelfte der Uploads mit 403 ab, das andere erlaubt auch + * `sicherungen/`. + * + * Die Spalte bleibt, was sie war -- ein Textfeld. Mehrere Werte stehen durch + * Komma getrennt darin. Damit gilt jeder BESTEHENDE Token unveraendert + * weiter: ein Wert ohne Komma ist eine Liste mit einem Eintrag. + */ +export function tokenPrefixes(token: Pick): string[] { + return (token.prefix ?? '') + .split(',') + .map((p) => p.trim().replace(/^\/+|\/+$/g, '')) + .filter(Boolean) +} + +/** Die Fesseln als Satzstueck fuer eine Fehlermeldung. + * + * Aus der rohen Spalte wuerde sonst `"vehicles,items/"` -- ein Ordner, den es + * nicht gibt. Wer eine 403 bekommt, soll lesen koennen, wohin er DARF. */ +export function fesselText(token: Pick): string { + const f = tokenPrefixes(token) + if (f.length === 0) return 'ueberall' + if (f.length === 1) return `"${f[0]}/"` + return f.map((p) => `"${p}/"`).join(' oder ') +} + export function tokenAllows(token: Token, path: string): boolean { - if (!token.prefix) return true - const prefix = token.prefix.replace(/\/+$/, '') + '/' - return path.startsWith(prefix) + const fesseln = tokenPrefixes(token) + // Keine Fessel heisst ueberall -- so war es, so bleibt es. + if (fesseln.length === 0) return true + // Das `/` gehoert dazu: ohne es wuerde "vehicles" auch "vehiclesXYZ/a.png" + // durchlassen. Stand schon vorher so da und bleibt. + return fesseln.some((p) => path.startsWith(p + '/')) } // -------------------------------------------------------------------- Start diff --git a/server/src/routes/dash.ts b/server/src/routes/dash.ts index 89724eb..f5c5085 100644 --- a/server/src/routes/dash.ts +++ b/server/src/routes/dash.ts @@ -2913,6 +2913,28 @@ dashRoutes.get('/tokens', (c) => { }) }) +/** Aus der Eingabe eine geprueste Fesselliste machen. + * + * Mehrere Ordner werden durch Komma getrennt. JEDER wird einzeln nach den + * Pfadregeln geprueft -- eine Liste, bei der ein Eintrag durchrutscht, ist + * keine Pruefung. Doppelte fallen weg, damit die Meldung spaeter nicht + * denselben Ordner zweimal nennt. + * + * Leer bleibt leer, und das heisst weiterhin "darf ueberall". + */ +function fesselnPruefen(roh: string): string { + const teile = roh + .split(',') + .map((t) => t.trim().replace(/^\/+|\/+$/g, '')) + .filter(Boolean) + const raus: string[] = [] + for (const t of teile) { + const sauber = checkPath(t) + if (!raus.includes(sauber)) raus.push(sauber) + } + return raus.join(',') +} + dashRoutes.post('/tokens', async (c) => { const body = await jsonBody<{ name?: string @@ -2996,7 +3018,7 @@ dashRoutes.post('/tokens', async (c) => { let cleanPrefix = '' if (prefix?.trim()) { try { - cleanPrefix = checkPath(prefix.trim().replace(/\/+$/, '')) + cleanPrefix = fesselnPruefen(prefix) } catch (err) { if (err instanceof PathError) return c.json({ error: err.message }, 400) throw err @@ -3082,7 +3104,7 @@ dashRoutes.patch('/tokens/:id', async (c) => { const roh = body.prefix.trim().replace(/^\/+|\/+$/g, '') if (roh) { try { - feld.prefix = checkPath(roh) + feld.prefix = fesselnPruefen(roh) } catch (err) { if (err instanceof PathError) return c.json({ error: `Praefix: ${err.message}` }, 400) throw err diff --git a/server/src/routes/fivemanage.ts b/server/src/routes/fivemanage.ts index 69b73d7..73ce667 100644 --- a/server/src/routes/fivemanage.ts +++ b/server/src/routes/fivemanage.ts @@ -30,7 +30,7 @@ import type { Context, Next } from 'hono' import { config } from '../config.js' import { meldeUpload } from '../meldung.js' import { db, logEvent, now, type Media, type Token } from '../db.js' -import { tokenAllows, tokenFromHeader } from '../auth.js' +import { fesselText, tokenAllows, tokenFromHeader } from '../auth.js' import { kontingentUeberschritten } from './upload.js' import { checkPath, @@ -252,7 +252,7 @@ async function ablegen( antwort: c.json( { status: 'error', - message: `dieser Token darf nur unter "${token.prefix}/" schreiben`, + message: `dieser Token darf nur unter ${fesselText(token)} schreiben`, }, 403, ), diff --git a/server/src/routes/upload.ts b/server/src/routes/upload.ts index feb4d25..6f163c8 100644 --- a/server/src/routes/upload.ts +++ b/server/src/routes/upload.ts @@ -24,7 +24,7 @@ import type { Context, Next } from 'hono' import { config } from '../config.js' import { meldeUpload } from '../meldung.js' import { db, logEvent, now, type Media, type Token } from '../db.js' -import { tokenAllows, tokenFromHeader } from '../auth.js' +import { fesselText, tokenAllows, tokenFromHeader } from '../auth.js' import { PathError, checkPath, @@ -236,7 +236,7 @@ uploadRoutes.post('/upload', async (c) => { if (!tokenAllows(token, path)) { return c.json( - { error: `dieser Token darf nur unter "${token.prefix}/" schreiben` }, + { error: `dieser Token darf nur unter ${fesselText(token)} schreiben` }, 403, ) } diff --git a/server/ui/src/pages/Tokens.tsx b/server/ui/src/pages/Tokens.tsx index 8509ff7..701c38f 100644 --- a/server/ui/src/pages/Tokens.tsx +++ b/server/ui/src/pages/Tokens.tsx @@ -98,10 +98,10 @@ export function Tokens() { onChange={(e) => setName(e.target.value)} /> setPrefix(e.target.value)} /> @@ -237,7 +237,7 @@ export function Tokens() { Name - Präfix + Ordner Rechte Grenzen {/* Was mit dem Token wirklich abgelegt wurde. @@ -266,7 +266,12 @@ export function Tokens() { )} - {t.prefix ? `${t.prefix}/` : '— (überall)'} + {t.prefix + ? t.prefix + .split(',') + .map((p) => `${p.trim()}/`) + .join(' · ') + : '— (überall)'} {t.can_delete ? (