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 <noreply@anthropic.com>
This commit is contained in:
+37
-3
@@ -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<Token, 'prefix'>): 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<Token, 'prefix'>): 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,10 +98,10 @@ export function Tokens() {
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Feld
|
||||
label="Präfix (optional)"
|
||||
label="Ordner (optional)"
|
||||
value={prefix}
|
||||
placeholder="vehicles"
|
||||
hinweis="Der Token darf dann nur darunter schreiben."
|
||||
placeholder="vehicles, items"
|
||||
hinweis="Nur unter diesen Ordnern darf der Token schreiben. Mehrere durch Komma. Leer = überall."
|
||||
onChange={(e) => setPrefix(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@@ -237,7 +237,7 @@ export function Tokens() {
|
||||
<thead>
|
||||
<tr className="text-left text-xs text-schrift-leise">
|
||||
<th className="pb-2 font-medium">Name</th>
|
||||
<th className="pb-2 font-medium">Präfix</th>
|
||||
<th className="pb-2 font-medium">Ordner</th>
|
||||
<th className="pb-2 font-medium">Rechte</th>
|
||||
<th className="pb-2 font-medium">Grenzen</th>
|
||||
{/* Was mit dem Token wirklich abgelegt wurde.
|
||||
@@ -266,7 +266,12 @@ export function Tokens() {
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 font-mono text-schrift-leise">
|
||||
{t.prefix ? `${t.prefix}/` : '— (überall)'}
|
||||
{t.prefix
|
||||
? t.prefix
|
||||
.split(',')
|
||||
.map((p) => `${p.trim()}/`)
|
||||
.join(' · ')
|
||||
: '— (überall)'}
|
||||
</td>
|
||||
<td className="py-2">
|
||||
{t.can_delete ? (
|
||||
|
||||
Reference in New Issue
Block a user