Fix Authentik permissions fallback and add debug endpoint
Deploy Standortplaner / deploy (push) Successful in 17s Details

This commit is contained in:
Johannes Baumeister 2026-07-06 11:43:20 +02:00
parent cf12184948
commit b81389d180
1 changed files with 57 additions and 20 deletions

View File

@ -38,21 +38,22 @@ function log(msg) {
// Hilfsfunktion zur Ermittlung der erlaubten Projekte aus Authentik-Headern // Hilfsfunktion zur Ermittlung der erlaubten Projekte aus Authentik-Headern
function getAllowedProjects(req) { function getAllowedProjects(req) {
const isProd = process.env.NODE_ENV === 'production'; 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 // Debug-Ausgabe der Authentik-Header
const authHeaders = {}; const authHeaders = {};
let hasAuthentikHeaders = false;
for (const key of Object.keys(req.headers)) { for (const key of Object.keys(req.headers)) {
if (key.startsWith('x-authentik-')) { if (key.startsWith('x-authentik-')) {
authHeaders[key] = req.headers[key]; authHeaders[key] = req.headers[key];
hasAuthentikHeaders = true;
} }
} }
if (Object.keys(authHeaders).length > 0) {
if (hasAuthentikHeaders) {
log(`Authentik Headers empfangen: ${JSON.stringify(authHeaders)}`); log(`Authentik Headers empfangen: ${JSON.stringify(authHeaders)}`);
} }
if (!username) { if (!hasAuthentikHeaders) {
// Lokale Entwicklung (Fallback) // Lokale Entwicklung (Fallback)
if (!isProd) { if (!isProd) {
return { return {
@ -65,6 +66,9 @@ function getAllowedProjects(req) {
return null; 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) // Projekte aus Attribut-Header parsen (Komma-separiert)
let allowedProjects = []; let allowedProjects = [];
if (projectsHeader) { if (projectsHeader) {
@ -74,7 +78,9 @@ function getAllowedProjects(req) {
// Gruppen aus Authentik-Header extrahieren und ebenfalls als Projekte behandeln // Gruppen aus Authentik-Header extrahieren und ebenfalls als Projekte behandeln
const groupsHeader = req.headers['x-authentik-groups']; const groupsHeader = req.headers['x-authentik-groups'];
if (groupsHeader) { 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) { for (const group of groups) {
// Alle Gruppen einfach als erlaubte Projekte hinzufügen. // Alle Gruppen einfach als erlaubte Projekte hinzufügen.
// Was kein Projekt ist (z.B. "portal-users"), wird später beim DB-Abgleich einfach ignoriert. // 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; 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 // Zuerst prüfen, ob die angeforderte ID/Name direkt im Header steht
const normalizedRequested = requestedProjectIdentifier.toLowerCase().trim(); const normalizedRequested = normalizeStr(requestedProjectIdentifier);
const hasDirectAccess = auth.projects.some(p => p.toLowerCase().trim() === normalizedRequested); const hasDirectAccess = auth.projects.some(p => normalizeStr(p) === normalizedRequested);
if (hasDirectAccess) { if (hasDirectAccess) {
return true; return true;
} }
@ -126,10 +138,9 @@ async function checkProjectAccess(req, requestedProjectIdentifier, client) {
projectName = res.rows[0].name; projectName = res.rows[0].name;
} }
} else { } else {
const normalized = requestedProjectIdentifier.toLowerCase().replace(/[-_]/g, '');
const res = await client.query( const res = await client.query(
'SELECT id, name FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\')) = $1', 'SELECT id, name FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\'), \' \', \'\')) = $1',
[normalized] [normalizedRequested]
); );
if (res.rows.length > 0) { if (res.rows.length > 0) {
projectId = res.rows[0].id; 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 // Prüfen, ob die aufgelöste ID oder der Projektname in der erlaubten Liste steht
const match = auth.projects.some(p => { const match = auth.projects.some(p => {
const normalizedP = p.toLowerCase().trim(); const normalizedP = normalizeStr(p);
return normalizedP === projectId.toLowerCase() || return normalizedP === normalizeStr(projectId) ||
normalizedP === (projectName || '').toLowerCase().trim(); normalizedP === normalizeStr(projectName);
}); });
return match; return match;
@ -172,11 +183,11 @@ app.get('/api/user/projekte', async (req, res) => {
projects = result.rows; projects = result.rows;
} else if (auth.projects.length > 0) { } 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 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( const result = await client.query(
`SELECT id, name FROM geodaten.projekte `SELECT id, name FROM geodaten.projekte
WHERE id::text = ANY($1::text[]) 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`, ORDER BY name`,
[originalIds, normalizedNames] [originalIds, normalizedNames]
); );
@ -186,7 +197,14 @@ app.get('/api/user/projekte', async (req, res) => {
res.json({ res.json({
username: auth.username, username: auth.username,
projects: projects, 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) { } catch (err) {
log(`FEHLER beim Laden der erlaubten Projekte: ${err.message}`); 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) => { app.get('/api/logs', (req, res) => {
res.type('text/plain').send(serverLogs.join('\n')); 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; 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; if (uuidRegex.test(input)) return input;
// Normalisierte Suche (Kleinschreibung, alle Bindestriche und Unterstriche entfernen) // Normalisierte Suche (Kleinschreibung, alle Bindestriche, Unterstriche und Leerzeichen entfernen)
const normalized = input.toLowerCase().replace(/[-_]/g, ''); const normalized = input.toLowerCase().replace(/[-_ ]/g, '');
const res = await client.query( 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] [normalized]
); );
return res.rows.length > 0 ? res.rows[0].id : null; 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(); const client = await pool.connect();
try { 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); const projId = await resolveProjectId(client, projectParam);
if (!projId) { if (!projId) {
return res.status(404).json({ error: 'Projekt nicht gefunden.' }); return res.status(404).json({ error: 'Projekt nicht gefunden.' });