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 */ static getAttributeValue(entry, attributeName) { const attr = entry.attributes.find(a => a.type === attributeName); if (!attr) { return null; } return Array.isArray(attr.values) ? attr.values[0] : attr.values; } /** * Escaped einen Wert für LDAP-Filter (verhindert LDAP-Injection) */ static escapeLDAPFilter(value) { if (!value) return ''; return value .replace(/\\/g, '\\5c') .replace(/\*/g, '\\2a') .replace(/\(/g, '\\28') .replace(/\)/g, '\\29') .replace(/\0/g, '\\00'); } /** * 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]; const username = ldapUser.username.trim(); const firstname = ldapUser.firstname.trim(); const lastname = ldapUser.lastname.trim(); // Prüfe ob Benutzer bereits existiert db.get('SELECT id, role FROM users WHERE username = ?', [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 db.run( 'UPDATE users SET firstname = ?, lastname = ? WHERE username = ?', [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 */ static authenticate(username, password, callback) { // 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'; const escapedUsername = this.escapeLDAPFilter(username); 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(); return callback(err, false); } res.on('searchEntry', (entry) => { userDN = entry.dn.toString(); }); res.on('error', (err) => { client.unbind(); callback(err, false); }); res.on('end', (result) => { // Service-Account-Verbindung schließen client.unbind(); if (!userDN) { return callback(new Error('Benutzer nicht gefunden'), 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) { return callback(new Error('Ungültiges Passwort'), 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;