210 lines
5.5 KiB
JavaScript
210 lines
5.5 KiB
JavaScript
import { randomUUID } from 'crypto';
|
|
import ldap from 'ldapjs';
|
|
|
|
let syncRunning = false;
|
|
|
|
function getAttr(entry, name) {
|
|
const want = String(name || '').toLowerCase();
|
|
for (const attr of entry.attributes) {
|
|
if (attr.type.toLowerCase() === want) {
|
|
const v = attr.values[0];
|
|
return v != null ? String(v).trim() : '';
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function searchAsync(client, base, options) {
|
|
return new Promise((resolve, reject) => {
|
|
const results = [];
|
|
client.search(base, options, (err, res) => {
|
|
if (err) return reject(err);
|
|
res.on('searchEntry', (entry) => results.push(entry));
|
|
res.on('error', reject);
|
|
res.on('end', () => resolve(results));
|
|
});
|
|
});
|
|
}
|
|
|
|
function bindAsync(client, dn, password) {
|
|
return new Promise((resolve, reject) => {
|
|
client.bind(dn || '', password ?? '', (err) => (err ? reject(err) : resolve()));
|
|
});
|
|
}
|
|
|
|
function unbindAsync(client) {
|
|
return new Promise((resolve) => {
|
|
try {
|
|
client.unbind(() => resolve());
|
|
} catch {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
function insertLog(db, row) {
|
|
db.prepare(
|
|
`INSERT INTO ldap_sync_log (id, started_at, finished_at, trigger_type, status, users_synced, error_message)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
).run(
|
|
row.id,
|
|
row.startedAt,
|
|
row.finishedAt,
|
|
row.triggerType,
|
|
row.status,
|
|
row.usersSynced,
|
|
row.errorMessage,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {import('node:sqlite').DatabaseSync} db
|
|
* @param {() => object} loadIntegrations
|
|
* @param {'manual' | 'automatic'} trigger
|
|
*/
|
|
export async function performLdapSync(db, loadIntegrations, trigger) {
|
|
if (syncRunning) {
|
|
return { skipped: true, message: 'Synchronisation läuft bereits.' };
|
|
}
|
|
syncRunning = true;
|
|
const logId = randomUUID();
|
|
const startedAt = new Date().toISOString();
|
|
let usersSynced = 0;
|
|
let errorMessage = null;
|
|
let client = null;
|
|
|
|
try {
|
|
const config = loadIntegrations().ldap || {};
|
|
const serverUrl = String(config.serverUrl || '').trim();
|
|
const searchBase = String(config.searchBase || '').trim();
|
|
const filter = String(
|
|
config.userSearchFilter || config.userFilter || '',
|
|
).trim();
|
|
const usernameAttr = String(
|
|
config.usernameAttribute || 'sAMAccountName',
|
|
).trim();
|
|
|
|
if (!serverUrl) throw new Error('LDAP-Server URL fehlt.');
|
|
if (!searchBase) throw new Error('Base DN fehlt.');
|
|
if (!filter) throw new Error('User Search Filter fehlt.');
|
|
|
|
client = ldap.createClient({
|
|
url: serverUrl,
|
|
timeout: 120000,
|
|
connectTimeout: 20000,
|
|
reconnect: false,
|
|
});
|
|
|
|
client.on('error', () => {});
|
|
|
|
const bindDn = String(config.bindDn || '').trim();
|
|
const bindPassword =
|
|
config.bindPassword != null ? String(config.bindPassword) : '';
|
|
|
|
await bindAsync(client, bindDn, bindPassword);
|
|
|
|
const entries = await searchAsync(client, searchBase, {
|
|
scope: 'sub',
|
|
filter,
|
|
attributes: [usernameAttr],
|
|
});
|
|
|
|
await unbindAsync(client);
|
|
client = null;
|
|
|
|
db.exec('BEGIN');
|
|
try {
|
|
let n = 0;
|
|
for (const entry of entries) {
|
|
let dn;
|
|
try {
|
|
dn = entry.objectName.toString();
|
|
} catch {
|
|
continue;
|
|
}
|
|
const un = getAttr(entry, usernameAttr).toLowerCase().trim();
|
|
if (!un) continue;
|
|
|
|
const existing = db
|
|
.prepare('SELECT id, source FROM users WHERE username = ?')
|
|
.get(un);
|
|
if (existing) {
|
|
if (existing.source === 'local') continue;
|
|
db.prepare(
|
|
`UPDATE users SET ldap_dn = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
).run(dn, existing.id);
|
|
n += 1;
|
|
} else {
|
|
const id = randomUUID();
|
|
db.prepare(
|
|
`INSERT INTO users (id, username, password_hash, role, source, ldap_dn, active, updated_at)
|
|
VALUES (?, ?, NULL, 'user', 'ldap', ?, 1, datetime('now'))`,
|
|
).run(id, un, dn);
|
|
n += 1;
|
|
}
|
|
}
|
|
db.exec('COMMIT');
|
|
usersSynced = n;
|
|
} catch (e) {
|
|
try {
|
|
db.exec('ROLLBACK');
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw e;
|
|
}
|
|
} catch (e) {
|
|
errorMessage = e && e.message ? String(e.message) : String(e);
|
|
if (client) {
|
|
try {
|
|
await unbindAsync(client);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
client = null;
|
|
}
|
|
} finally {
|
|
try {
|
|
const finishedAt = new Date().toISOString();
|
|
const status = errorMessage ? 'error' : 'success';
|
|
insertLog(db, {
|
|
id: logId,
|
|
startedAt,
|
|
finishedAt,
|
|
triggerType: trigger,
|
|
status,
|
|
usersSynced: errorMessage ? 0 : usersSynced,
|
|
errorMessage,
|
|
});
|
|
} finally {
|
|
syncRunning = false;
|
|
}
|
|
}
|
|
|
|
if (errorMessage) {
|
|
return { ok: false, usersSynced: 0, error: errorMessage };
|
|
}
|
|
return { ok: true, usersSynced };
|
|
}
|
|
|
|
/**
|
|
* @param {import('node:sqlite').DatabaseSync} db
|
|
*/
|
|
export function getSyncStatus(db) {
|
|
const last = db
|
|
.prepare(
|
|
`SELECT finished_at FROM ldap_sync_log ORDER BY finished_at DESC LIMIT 1`,
|
|
)
|
|
.get();
|
|
const entries = db
|
|
.prepare(
|
|
`SELECT finished_at AS finishedAt, trigger_type AS triggerType, status, users_synced AS usersSynced, error_message AS errorMessage
|
|
FROM ldap_sync_log ORDER BY finished_at DESC LIMIT 10`,
|
|
)
|
|
.all();
|
|
return {
|
|
lastSyncAt: last?.finished_at ?? null,
|
|
entries,
|
|
};
|
|
}
|