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
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.' });