Kundenansicht: Terminplaner-POSTs nur noch ueber eine explizite Liste
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a72380cc8b
commit
8c1055e8a0
2 files changed
+58
-4
No files matched your search
@@ -26,6 +26,7 @@ const { kundenansichtGuard, KUNDENANSICHT_ALLOWED } = await import('../server/mi
|
||||
const { createStaffPreviewToken } = await import('../server/services/staffPreviewTokens.js')
|
||||
|
||||
// Kunden-Router mit denselben Mount-Pfaden wie server/index.js
|
||||
const termineRouter = (await import('../server/routes/termine.js')).default
|
||||
const MOUNTS = [
|
||||
['/api/auth', (await import('../server/routes/auth.js')).default],
|
||||
['/api/projects', (await import('../server/routes/projects.js')).default],
|
||||
@@ -36,7 +37,7 @@ const MOUNTS = [
|
||||
['/api/abo/takeover-credits', (await import('../server/routes/takeoverCredits.js')).default],
|
||||
['/api/abo', (await import('../server/routes/abo.js')).default],
|
||||
['/api/funktionen', (await import('../server/routes/funktionen.js')).default],
|
||||
['/api/termine', (await import('../server/routes/termine.js')).default],
|
||||
['/api/termine', termineRouter],
|
||||
]
|
||||
// Oeffentliche Schnittstelle (ohne Session) wie in server/index.js hinter dem Waechter; nicht in MOUNTS, weil sie keine Kunden-Route ist
|
||||
const oeffentlich = (await import('../server/routes/publicTermine.js')).default
|
||||
@@ -113,11 +114,15 @@ test('Erlaubnisliste: genau News, Terminplaner, Analyse-Link, Ende, Logout und L
|
||||
['DELETE', '/api/termine/buchungen/abc'], ['DELETE', '/api/termine/buchungen/abc/'],
|
||||
]
|
||||
for (const [m, p] of ok) assert.ok(allowed(m, p), `${m} ${p}`)
|
||||
assert.equal(KUNDENANSICHT_ALLOWED.length, 8 + 3, 'genau drei Termine-Eintraege dazu')
|
||||
assert.equal(KUNDENANSICHT_ALLOWED.length, 8 + 6, 'genau sechs Termine-Eintraege dazu, keine Wildcard')
|
||||
for (const [m, p] of [
|
||||
['POST', '/api/chat/read'], ['POST', '/api/news/abc'], ['POST', '/api/auth/password'], ['POST', '/api/abo/checkout'], ['POST', '/api/funktionen/news/checkout'],
|
||||
// Terminplaner: nur der eigene Pfad, nur diese Methoden und Formen
|
||||
['POST', '/api/termineX'], ['POST', '/api/termineX/buchungen'], ['POST', '/api/termine'], ['POST', '/api/termine/'],
|
||||
// frueher durch die Wildcard POST /api/termine/.+ erlaubt, jetzt nur noch die ausgeschriebenen Pfade
|
||||
['POST', '/api/termine/zukunft'], ['POST', '/api/termine/fragebogen'], ['PUT', '/api/termine/fragebogen'], ['POST', '/api/termine/export.csv'],
|
||||
['POST', '/api/termine/buchungen/abc'], ['POST', '/api/termine/buchungen/abc/aktion/x'], ['POST', '/api/termine/einstellungen/vorlage/x'],
|
||||
['POST', '/api/termine/ressourcen/abc/feed/x'], ['POST', '/api/funktionen/termine/checkout'],
|
||||
['PUT', '/api/termine/einstellungen'], ['PATCH', '/api/termine/buchungen/abc'], ['PATCH', '/api/termine/einstellungen/x'],
|
||||
['DELETE', '/api/termine/einstellungen'], ['DELETE', '/api/termine/buchungen'], ['DELETE', '/api/termine/buchungen/abc/def'],
|
||||
['DELETE', '/api/termine/ressourcen/abc'], ['PUT', '/api/termine/buchungen/abc'],
|
||||
@@ -126,6 +131,50 @@ test('Erlaubnisliste: genau News, Terminplaner, Analyse-Link, Ende, Logout und L
|
||||
}
|
||||
})
|
||||
|
||||
// Spec 2026-10-03, Abschnitt 3: unbekannte Schreib-Pfade unter /api/termine sind in der Kundenansicht gesperrt, bevor der Router
|
||||
// sie sieht (auch Pfade ohne Route und Gross-/Kleinschreibung); normale Kunden beruehrt der Waechter nicht
|
||||
test('unbekanntes POST/PUT unter /api/termine in der Kundenansicht -> 403 view_blocked, normale Kunden nicht', async () => {
|
||||
const faelle = [
|
||||
['POST', '/api/termine/zukunft'],
|
||||
['PUT', '/api/termine/fragebogen'],
|
||||
['POST', '/api/termine/buchungen/abc/aktion/x'],
|
||||
['POST', '/API/Termine/Zukunft'],
|
||||
]
|
||||
for (const [method, p] of faelle) {
|
||||
const gesperrt = await call(method, p, { ...kv(), body: {} })
|
||||
assert.equal(gesperrt.status, 403, `${method} ${p}`)
|
||||
assert.equal(gesperrt.body.code, 'view_blocked', `${method} ${p}`)
|
||||
const kunde = await call(method, p, { cookie: customerCookie('c1'), body: {} })
|
||||
assert.notEqual(kunde.status, 403, `${method} ${p}: normaler Kunde`)
|
||||
// PUT /fragebogen bekommt in Block C eine echte Route (dann antwortet sie selbst); die anderen Pfade gibt es nie
|
||||
if (!p.endsWith('/fragebogen')) assert.equal(kunde.status, 404, `${method} ${p}: der Router kennt den Pfad nicht`)
|
||||
}
|
||||
})
|
||||
|
||||
// Router-Abgleich: die Erlaubnisliste und die echten Schreib-Routen des Terminplaners decken sich genau; eine neue Route faellt
|
||||
// im Test "jede Schreib-Route ausserhalb der Erlaubnisliste -> 403" auf (sie muss gesperrt sein), ein toter Eintrag hier
|
||||
test('Router-Abgleich: genau sechs Schreib-Routen des Terminplaners stehen auf der Erlaubnisliste', () => {
|
||||
const erlaubt = writeRoutes([['/api/termine', termineRouter]])
|
||||
.filter(({ method, path: p }) => allowed(method, p))
|
||||
.map(({ method, path: p }) => `${method} ${p}`)
|
||||
.sort()
|
||||
assert.deepEqual(erlaubt, [
|
||||
'DELETE /api/termine/buchungen/x',
|
||||
'PATCH /api/termine/einstellungen',
|
||||
'POST /api/termine/buchungen',
|
||||
'POST /api/termine/buchungen/x/aktion',
|
||||
'POST /api/termine/einstellungen/vorlage',
|
||||
'POST /api/termine/ressourcen/x/feed',
|
||||
])
|
||||
// und jeder Termine-Eintrag der Liste trifft genau eine echte Route (kein toter, kein doppelter Eintrag)
|
||||
const routen = writeRoutes([['/api/termine', termineRouter]])
|
||||
const termineEintraege = KUNDENANSICHT_ALLOWED.filter(([, re]) => re.source.includes('termine'))
|
||||
assert.equal(termineEintraege.length, 6)
|
||||
for (const [m, re] of termineEintraege) {
|
||||
assert.equal(routen.filter((r) => r.method === m && re.test(r.path)).length, 1, `${m} ${re}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('POST /api/chat/read ist gesperrt (Lesestatus bleibt unberuehrt)', async () => {
|
||||
const res = await call('POST', '/api/chat/read', kv())
|
||||
assert.equal(res.status, 403)
|
||||
|
||||
Reference in new issue
Block a user