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? */
|
/** 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 {
|
export function tokenAllows(token: Token, path: string): boolean {
|
||||||
if (!token.prefix) return true
|
const fesseln = tokenPrefixes(token)
|
||||||
const prefix = token.prefix.replace(/\/+$/, '') + '/'
|
// Keine Fessel heisst ueberall -- so war es, so bleibt es.
|
||||||
return path.startsWith(prefix)
|
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
|
// -------------------------------------------------------------------- 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) => {
|
dashRoutes.post('/tokens', async (c) => {
|
||||||
const body = await jsonBody<{
|
const body = await jsonBody<{
|
||||||
name?: string
|
name?: string
|
||||||
@@ -2996,7 +3018,7 @@ dashRoutes.post('/tokens', async (c) => {
|
|||||||
let cleanPrefix = ''
|
let cleanPrefix = ''
|
||||||
if (prefix?.trim()) {
|
if (prefix?.trim()) {
|
||||||
try {
|
try {
|
||||||
cleanPrefix = checkPath(prefix.trim().replace(/\/+$/, ''))
|
cleanPrefix = fesselnPruefen(prefix)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof PathError) return c.json({ error: err.message }, 400)
|
if (err instanceof PathError) return c.json({ error: err.message }, 400)
|
||||||
throw err
|
throw err
|
||||||
@@ -3082,7 +3104,7 @@ dashRoutes.patch('/tokens/:id', async (c) => {
|
|||||||
const roh = body.prefix.trim().replace(/^\/+|\/+$/g, '')
|
const roh = body.prefix.trim().replace(/^\/+|\/+$/g, '')
|
||||||
if (roh) {
|
if (roh) {
|
||||||
try {
|
try {
|
||||||
feld.prefix = checkPath(roh)
|
feld.prefix = fesselnPruefen(roh)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof PathError) return c.json({ error: `Praefix: ${err.message}` }, 400)
|
if (err instanceof PathError) return c.json({ error: `Praefix: ${err.message}` }, 400)
|
||||||
throw err
|
throw err
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import type { Context, Next } from 'hono'
|
|||||||
import { config } from '../config.js'
|
import { config } from '../config.js'
|
||||||
import { meldeUpload } from '../meldung.js'
|
import { meldeUpload } from '../meldung.js'
|
||||||
import { db, logEvent, now, type Media, type Token } from '../db.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 { kontingentUeberschritten } from './upload.js'
|
||||||
import {
|
import {
|
||||||
checkPath,
|
checkPath,
|
||||||
@@ -252,7 +252,7 @@ async function ablegen(
|
|||||||
antwort: c.json(
|
antwort: c.json(
|
||||||
{
|
{
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: `dieser Token darf nur unter "${token.prefix}/" schreiben`,
|
message: `dieser Token darf nur unter ${fesselText(token)} schreiben`,
|
||||||
},
|
},
|
||||||
403,
|
403,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import type { Context, Next } from 'hono'
|
|||||||
import { config } from '../config.js'
|
import { config } from '../config.js'
|
||||||
import { meldeUpload } from '../meldung.js'
|
import { meldeUpload } from '../meldung.js'
|
||||||
import { db, logEvent, now, type Media, type Token } from '../db.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 {
|
import {
|
||||||
PathError,
|
PathError,
|
||||||
checkPath,
|
checkPath,
|
||||||
@@ -236,7 +236,7 @@ uploadRoutes.post('/upload', async (c) => {
|
|||||||
|
|
||||||
if (!tokenAllows(token, path)) {
|
if (!tokenAllows(token, path)) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{ error: `dieser Token darf nur unter "${token.prefix}/" schreiben` },
|
{ error: `dieser Token darf nur unter ${fesselText(token)} schreiben` },
|
||||||
403,
|
403,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,10 +98,10 @@ export function Tokens() {
|
|||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Feld
|
<Feld
|
||||||
label="Präfix (optional)"
|
label="Ordner (optional)"
|
||||||
value={prefix}
|
value={prefix}
|
||||||
placeholder="vehicles"
|
placeholder="vehicles, items"
|
||||||
hinweis="Der Token darf dann nur darunter schreiben."
|
hinweis="Nur unter diesen Ordnern darf der Token schreiben. Mehrere durch Komma. Leer = überall."
|
||||||
onChange={(e) => setPrefix(e.target.value)}
|
onChange={(e) => setPrefix(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +237,7 @@ export function Tokens() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs text-schrift-leise">
|
<tr className="text-left text-xs text-schrift-leise">
|
||||||
<th className="pb-2 font-medium">Name</th>
|
<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">Rechte</th>
|
||||||
<th className="pb-2 font-medium">Grenzen</th>
|
<th className="pb-2 font-medium">Grenzen</th>
|
||||||
{/* Was mit dem Token wirklich abgelegt wurde.
|
{/* Was mit dem Token wirklich abgelegt wurde.
|
||||||
@@ -266,7 +266,12 @@ export function Tokens() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 font-mono text-schrift-leise">
|
<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>
|
||||||
<td className="py-2">
|
<td className="py-2">
|
||||||
{t.can_delete ? (
|
{t.can_delete ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user