From b81389d180ad794537fd91d2dd083d681ad6213a Mon Sep 17 00:00:00 2001 From: Johannes Baumeister Date: Mon, 6 Jul 2026 11:43:20 +0200 Subject: [PATCH] Fix Authentik permissions fallback and add debug endpoint --- server.js | 77 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/server.js b/server.js index 5b10f4e..307ad1e 100644 --- a/server.js +++ b/server.js @@ -38,21 +38,22 @@ function log(msg) { // Hilfsfunktion zur Ermittlung der erlaubten Projekte aus Authentik-Headern function getAllowedProjects(req) { const isProd = process.env.NODE_ENV === 'production'; - const username = req.headers['x-authentik-username']; - const projectsHeader = req.headers['x-authentik-meta-projekte']; - + // Debug-Ausgabe der Authentik-Header const authHeaders = {}; + let hasAuthentikHeaders = false; for (const key of Object.keys(req.headers)) { if (key.startsWith('x-authentik-')) { authHeaders[key] = req.headers[key]; + hasAuthentikHeaders = true; } } - if (Object.keys(authHeaders).length > 0) { + + if (hasAuthentikHeaders) { log(`Authentik Headers empfangen: ${JSON.stringify(authHeaders)}`); } - if (!username) { + if (!hasAuthentikHeaders) { // Lokale Entwicklung (Fallback) if (!isProd) { return { @@ -65,6 +66,9 @@ function getAllowedProjects(req) { return null; } + const username = req.headers['x-authentik-username'] || req.headers['x-authentik-email'] || req.headers['x-authentik-name'] || 'authentik-user'; + const projectsHeader = req.headers['x-authentik-meta-projekte']; + // Projekte aus Attribut-Header parsen (Komma-separiert) let allowedProjects = []; if (projectsHeader) { @@ -74,7 +78,9 @@ function getAllowedProjects(req) { // Gruppen aus Authentik-Header extrahieren und ebenfalls als Projekte behandeln const groupsHeader = req.headers['x-authentik-groups']; if (groupsHeader) { - const groups = groupsHeader.split('|').map(g => g.trim()).filter(Boolean); + // Authentik might use | or , depending on proxy settings. Replace commas with pipes to be safe. + const normalizedGroupsHeader = groupsHeader.replace(/,/g, '|'); + const groups = normalizedGroupsHeader.split('|').map(g => g.trim()).filter(Boolean); for (const group of groups) { // Alle Gruppen einfach als erlaubte Projekte hinzufügen. // Was kein Projekt ist (z.B. "portal-users"), wird später beim DB-Abgleich einfach ignoriert. @@ -105,9 +111,15 @@ async function checkProjectAccess(req, requestedProjectIdentifier, client) { return false; } + // Hilfsfunktion zum Normalisieren (entfernt Bindestriche, Unterstriche und Leerzeichen) + const normalizeStr = (str) => { + if (!str) return ''; + return str.toLowerCase().replace(/[-_ ]/g, '').trim(); + }; + // Zuerst prüfen, ob die angeforderte ID/Name direkt im Header steht - const normalizedRequested = requestedProjectIdentifier.toLowerCase().trim(); - const hasDirectAccess = auth.projects.some(p => p.toLowerCase().trim() === normalizedRequested); + const normalizedRequested = normalizeStr(requestedProjectIdentifier); + const hasDirectAccess = auth.projects.some(p => normalizeStr(p) === normalizedRequested); if (hasDirectAccess) { return true; } @@ -126,10 +138,9 @@ async function checkProjectAccess(req, requestedProjectIdentifier, client) { projectName = res.rows[0].name; } } else { - const normalized = requestedProjectIdentifier.toLowerCase().replace(/[-_]/g, ''); const res = await client.query( - 'SELECT id, name FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\')) = $1', - [normalized] + 'SELECT id, name FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\'), \' \', \'\')) = $1', + [normalizedRequested] ); if (res.rows.length > 0) { projectId = res.rows[0].id; @@ -141,9 +152,9 @@ async function checkProjectAccess(req, requestedProjectIdentifier, client) { // Prüfen, ob die aufgelöste ID oder der Projektname in der erlaubten Liste steht const match = auth.projects.some(p => { - const normalizedP = p.toLowerCase().trim(); - return normalizedP === projectId.toLowerCase() || - normalizedP === (projectName || '').toLowerCase().trim(); + const normalizedP = normalizeStr(p); + return normalizedP === normalizeStr(projectId) || + normalizedP === normalizeStr(projectName); }); return match; @@ -172,11 +183,11 @@ app.get('/api/user/projekte', async (req, res) => { projects = result.rows; } else if (auth.projects.length > 0) { const originalIds = auth.projects.filter(p => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(p)); - const normalizedNames = auth.projects.map(p => p.toLowerCase().replace(/[-_]/g, '').trim()); + const normalizedNames = auth.projects.map(p => p.toLowerCase().replace(/[-_ ]/g, '').trim()); const result = await client.query( `SELECT id, name FROM geodaten.projekte WHERE id::text = ANY($1::text[]) - OR LOWER(REPLACE(REPLACE(name, '-', ''), '_', '')) = ANY($2::text[]) + OR LOWER(REPLACE(REPLACE(REPLACE(name, '-', ''), '_', ''), ' ', '')) = ANY($2::text[]) ORDER BY name`, [originalIds, normalizedNames] ); @@ -186,7 +197,14 @@ app.get('/api/user/projekte', async (req, res) => { res.json({ username: auth.username, projects: projects, - isLocalDev: auth.isLocalDev + isLocalDev: auth.isLocalDev, + debug_raw_groups: req.headers['x-authentik-groups'] || null, + debug_raw_headers: Object.keys(req.headers) + .filter(k => k.startsWith('x-authentik-')) + .reduce((obj, key) => { + obj[key] = req.headers[key]; + return obj; + }, {}) }); } catch (err) { log(`FEHLER beim Laden der erlaubten Projekte: ${err.message}`); @@ -211,6 +229,19 @@ app.get('/api/status', async (req, res) => { } }); +// Debug-Endpunkt für HTTP Headers (hilfreich für Authentik-Fehlersuche) +app.get('/api/debug/headers', (req, res) => { + res.json({ + all_headers: req.headers, + authentik_headers: Object.keys(req.headers) + .filter(k => k.startsWith('x-authentik-')) + .reduce((obj, key) => { + obj[key] = req.headers[key]; + return obj; + }, {}) + }); +}); + app.get('/api/logs', (req, res) => { res.type('text/plain').send(serverLogs.join('\n')); }); @@ -321,11 +352,11 @@ async function resolveProjectId(client, input) { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (uuidRegex.test(input)) return input; - // Normalisierte Suche (Kleinschreibung, alle Bindestriche und Unterstriche entfernen) - const normalized = input.toLowerCase().replace(/[-_]/g, ''); + // Normalisierte Suche (Kleinschreibung, alle Bindestriche, Unterstriche und Leerzeichen entfernen) + const normalized = input.toLowerCase().replace(/[-_ ]/g, ''); const res = await client.query( - 'SELECT id FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\')) = $1', + 'SELECT id FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\'), \' \', \'\')) = $1', [normalized] ); return res.rows.length > 0 ? res.rows[0].id : null; @@ -691,6 +722,12 @@ app.get('/api/layers/leitungsverlaeufe', async (req, res) => { const client = await pool.connect(); try { + const hasAccess = await checkProjectAccess(req, projectParam, client); + if (!hasAccess) { + log(`Zugriff verweigert für Leitungsverläufe in Projekt: ${projectParam}`); + return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' }); + } + const projId = await resolveProjectId(client, projectParam); if (!projId) { return res.status(404).json({ error: 'Projekt nicht gefunden.' });