const ldap = require('ldapjs'); const { db } = require('./database'); const bcrypt = require('bcryptjs'); /** * LDAP-Service für Benutzer-Synchronisation */ class LDAPService { /** * LDAP-Konfiguration aus der Datenbank abrufen */ static getConfig(callback) { db.get('SELECT * FROM ldap_config WHERE id = 1', (err, config) => { if (err) { return callback(err, null); } callback(null, config); }); } /** * LDAP-Verbindung herstellen */ static connect(config, callback) { if (!config || !config.enabled || !config.url) { return callback(new Error('LDAP ist nicht konfiguriert oder deaktiviert')); } const client = ldap.createClient({ url: config.url, timeout: 10000, connectTimeout: 10000 }); // Fehlerbehandlung client.on('error', (err) => { callback(err, null); }); // Bind mit Credentials const bindDN = config.bind_dn || ''; const bindPassword = config.bind_password || ''; // Hinweis: Passwort wird im Klartext gespeichert // In einer produktiven Umgebung sollte man eine Verschlüsselung mit einem Master-Key verwenden client.bind(bindDN, bindPassword, (err) => { if (err) { client.unbind(); return callback(err, null); } callback(null, client); }); } /** * Benutzer aus LDAP abrufen */ static searchUsers(client, config, callback) { const baseDN = config.base_dn || ''; const searchFilter = config.user_search_filter || '(objectClass=person)'; const searchOptions = { filter: searchFilter, scope: 'sub', attributes: [ config.username_attribute || 'cn', config.firstname_attribute || 'givenName', config.lastname_attribute || 'sn' ] }; const users = []; client.search(baseDN, searchOptions, (err, res) => { if (err) { return callback(err, null); } res.on('searchEntry', (entry) => { const user = { username: this.getAttributeValue(entry, config.username_attribute || 'cn'), firstname: this.getAttributeValue(entry, config.firstname_attribute || 'givenName'), lastname: this.getAttributeValue(entry, config.lastname_attribute || 'sn') }; // Nur Benutzer mit allen erforderlichen Feldern hinzufügen if (user.username && user.firstname && user.lastname) { users.push(user); } }); res.on('error', (err) => { callback(err, null); }); res.on('end', (result) => { if (result && result.status !== 0) { return callback(new Error(`LDAP-Suche fehlgeschlagen: ${result.status}`), null); } callback(null, users); }); }); } /** * Wert eines LDAP-Attributs extrahieren * * Die ldapjs-Bibliothek behandelt UTF-8-Zeichen automatisch korrekt. * Diese Funktion stellt sicher, dass UTF-8-Zeichen wie ß, ä, ö, ü korrekt zurückgegeben werden. */ static getAttributeValue(entry, attributeName) { const attr = entry.attributes.find(a => a.type === attributeName); if (!attr) { return null; } const value = Array.isArray(attr.values) ? attr.values[0] : attr.values; // Stelle sicher, dass der Wert als String zurückgegeben wird (UTF-8 wird automatisch korrekt behandelt) return value != null ? String(value) : null; } /** * Escaped einen Wert für LDAP-Filter (verhindert LDAP-Injection) * * WICHTIG: UTF-8-Zeichen wie ß, ä, ö, ü müssen NICHT escaped werden. * LDAP-Filter unterstützen UTF-8 direkt nach RFC 4515. * Nur die speziellen LDAP-Filter-Zeichen werden escaped. */ static escapeLDAPFilter(value) { if (!value) return ''; // Stelle sicher, dass der Wert als String behandelt wird const str = String(value); // Escape nur die speziellen LDAP-Filter-Zeichen // UTF-8-Zeichen wie ß, ä, ö, ü werden direkt verwendet return str .replace(/\\/g, '\\5c') // Backslash .replace(/\*/g, '\\2a') // Stern .replace(/\(/g, '\\28') // Öffnende Klammer .replace(/\)/g, '\\29') // Schließende Klammer .replace(/\0/g, '\\00'); // Null-Byte } /** * Benutzer in SQLite synchronisieren */ static syncUsers(ldapUsers, callback) { let syncedCount = 0; let errorCount = 0; const errors = []; if (!ldapUsers || ldapUsers.length === 0) { return callback(null, { synced: 0, errors: [] }); } // Verarbeite jeden Benutzer const processUser = (index) => { if (index >= ldapUsers.length) { return callback(null, { synced: syncedCount, errors: errors }); } const ldapUser = ldapUsers[index]; // .trim() behält UTF-8-Zeichen wie ß, ä, ö, ü korrekt bei // Stelle sicher, dass Werte als String behandelt werden const username = String(ldapUser.username || '').trim(); const firstname = String(ldapUser.firstname || '').trim(); const lastname = String(ldapUser.lastname || '').trim(); // Prüfe ob Benutzer bereits existiert (case-insensitive) db.get('SELECT id, role FROM users WHERE username = ? COLLATE NOCASE', [username], (err, existingUser) => { if (err) { errors.push(`Fehler beim Prüfen von ${username}: ${err.message}`); errorCount++; return processUser(index + 1); } if (existingUser) { // Benutzer existiert - aktualisiere nur Name, behalte Rolle (case-insensitive) db.run( 'UPDATE users SET firstname = ?, lastname = ? WHERE username = ? COLLATE NOCASE', [firstname, lastname, username], (err) => { if (err) { errors.push(`Fehler beim Aktualisieren von ${username}: ${err.message}`); errorCount++; } else { syncedCount++; } processUser(index + 1); } ); } else { // Neuer Benutzer - erstelle mit Standard-Rolle // Generiere ein zufälliges Passwort (Benutzer muss es beim ersten Login ändern) const defaultPassword = bcrypt.hashSync('changeme123', 10); db.run( 'INSERT INTO users (username, password, firstname, lastname, role) VALUES (?, ?, ?, ?, ?)', [username, defaultPassword, firstname, lastname, 'mitarbeiter'], (err) => { if (err) { errors.push(`Fehler beim Erstellen von ${username}: ${err.message}`); errorCount++; } else { syncedCount++; } processUser(index + 1); } ); } }); }; processUser(0); } /** * Sync-Log-Eintrag erstellen */ static createSyncLog(syncType, status, usersSynced, errorMessage, callback) { const startedAt = new Date().toISOString(); const completedAt = new Date().toISOString(); db.run( `INSERT INTO ldap_sync_log (sync_type, status, users_synced, error_message, sync_started_at, sync_completed_at) VALUES (?, ?, ?, ?, ?, ?)`, [syncType, status, usersSynced, errorMessage || null, startedAt, completedAt], (err) => { if (callback) { callback(err); } } ); } /** * Letzte Synchronisation aktualisieren */ static updateLastSync(callback) { db.run( 'UPDATE ldap_config SET last_sync = CURRENT_TIMESTAMP WHERE id = 1', (err) => { if (callback) { callback(err); } } ); } /** * Benutzer gegen LDAP authentifizieren * * Unterstützt UTF-8-Zeichen wie ß, ä, ö, ü in Usernamen. * Die ldapjs-Bibliothek behandelt UTF-8 automatisch korrekt. */ static authenticate(username, password, callback) { // Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt) const usernameStr = String(username || '').trim(); if (!usernameStr) { return callback(new Error('Benutzername darf nicht leer sein'), false); } // Konfiguration abrufen this.getConfig((err, config) => { if (err || !config || !config.enabled) { return callback(new Error('LDAP ist nicht aktiviert'), false); } // LDAP-Verbindung herstellen (mit Service-Account) this.connect(config, (err, client) => { if (err) { return callback(err, false); } // Suche nach dem Benutzer in LDAP const baseDN = config.base_dn || ''; const usernameAttr = config.username_attribute || 'cn'; // escapeLDAPFilter behandelt UTF-8-Zeichen korrekt (escaped sie nicht) const escapedUsername = this.escapeLDAPFilter(usernameStr); const searchFilter = `(${usernameAttr}=${escapedUsername})`; const searchOptions = { filter: searchFilter, scope: 'sub', attributes: ['dn', usernameAttr] }; let userDN = null; client.search(baseDN, searchOptions, (err, res) => { if (err) { client.unbind(); // Verbesserte Fehlermeldung für mögliche Encoding-Probleme const errorMsg = err.message || String(err); return callback(new Error(`LDAP-Suche fehlgeschlagen: ${errorMsg}. Hinweis: Prüfen Sie, ob der Benutzername UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt enthält.`), false); } res.on('searchEntry', (entry) => { userDN = entry.dn.toString(); }); res.on('error', (err) => { client.unbind(); const errorMsg = err.message || String(err); callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false); }); res.on('end', (result) => { // Service-Account-Verbindung schließen client.unbind(); if (!userDN) { // Verbesserte Fehlermeldung: Hinweis auf mögliche Encoding-Probleme return callback(new Error(`Benutzer "${usernameStr}" nicht gefunden. Hinweis: Prüfen Sie, ob der Benutzername korrekt ist und UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt geschrieben sind.`), false); } // Versuche, sich mit den Benutzer-Credentials zu binden const authClient = ldap.createClient({ url: config.url, timeout: 10000, connectTimeout: 10000 }); authClient.on('error', (err) => { authClient.unbind(); callback(err, false); }); authClient.bind(userDN, password, (err) => { authClient.unbind(); if (err) { const errorMsg = err.message || String(err); return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false); } callback(null, true); }); }); }); }); }); } /** * Vollständige Synchronisation durchführen */ static performSync(syncType, callback) { const startedAt = new Date(); // Konfiguration abrufen this.getConfig((err, config) => { if (err) { this.createSyncLog(syncType, 'error', 0, `Fehler beim Abrufen der Konfiguration: ${err.message}`, () => {}); return callback(err); } if (!config || !config.enabled) { const errorMsg = 'LDAP-Synchronisation ist nicht aktiviert'; this.createSyncLog(syncType, 'error', 0, errorMsg, () => {}); return callback(new Error(errorMsg)); } // LDAP-Verbindung herstellen this.connect(config, (err, client) => { if (err) { this.createSyncLog(syncType, 'error', 0, `LDAP-Verbindungsfehler: ${err.message}`, () => {}); return callback(err); } // Benutzer aus LDAP abrufen this.searchUsers(client, config, (err, ldapUsers) => { // Verbindung schließen client.unbind(); if (err) { this.createSyncLog(syncType, 'error', 0, `LDAP-Suchfehler: ${err.message}`, () => {}); return callback(err); } // Benutzer synchronisieren this.syncUsers(ldapUsers, (err, result) => { if (err) { this.createSyncLog(syncType, 'error', result.synced, `Sync-Fehler: ${err.message}`, () => {}); return callback(err); } // Letzte Synchronisation aktualisieren this.updateLastSync(() => { const status = result.errors.length > 0 ? 'error' : 'success'; const errorMsg = result.errors.length > 0 ? result.errors.join('; ') : null; this.createSyncLog(syncType, status, result.synced, errorMsg, () => { callback(null, result); }); }); }); }); }); }); } } module.exports = LDAPService;