const ldap = require('ldapjs'); const fs = require('fs'); const csv = require('csv-parser'); const { Parser } = require('json2csv'); const iconv = require('iconv-lite'); // Shift-JIS 変換用 const LDAP_URL = 'ldap://192.168.4.225'; const BASE_DN = 'dc=ldap,dc=next-hd,dc=co,dc=jp'; const BIND_DN = 'uid=root,cn=users,dc=ldap,dc=next-hd,dc=co,dc=jp'; const BIND_PASSWORD = 'il7DXhKA'; function padTwo(n) { return String(n).padStart(2, '0'); } // 現在日時からエクスポート用タイムスタンプ文字列を生成 function createTimestamp() { const now = new Date(); return `${now.getFullYear()}${padTwo(now.getMonth() + 1)}${padTwo(now.getDate())}_${padTwo(now.getHours())}${padTwo(now.getMinutes())}${padTwo(now.getSeconds())}`; } const EXPORT_CSV = `ldap_users_export_${createTimestamp()}.csv`; const IMPORT_CSV = 'ldap_users_import.csv'; // LDAP クライアントに bind して認証を完了する Promise function bindClient(client) { return new Promise(function (resolve, reject) { client.bind(BIND_DN, BIND_PASSWORD, function (err) { if (err) { reject(err); return; } resolve(); }); }); } // bind 済みの LDAP クライアントを生成 async function createBoundClient() { const client = ldap.createClient({ url: LDAP_URL }); await bindClient(client); return client; } // searchEntry から通常オブジェクトに変換 function entryToObject(entry) { if (!entry || !Array.isArray(entry.attributes)) { return null; } const obj = {}; entry.attributes.forEach(function (attr) { obj[attr.type] = Array.isArray(attr.vals) ? attr.vals.join(',') : attr.vals; }); return obj; } // LDAP 検索を行いエントリ配列を取得 function searchEntries(client, options) { return new Promise(function (resolve, reject) { client.search(BASE_DN, options, function (err, res) { if (err) { reject(err); return; } const results = []; res.on('searchEntry', function (entry) { results.push(entry); }); res.on('error', function (searchErr) { reject(searchErr); }); res.on('end', function () { resolve(results); }); }); }); } // エントリの属性を変更 function modifyEntry(client, dn, changes) { return new Promise(function (resolve, reject) { client.modify(dn, changes, function (err) { if (err) { reject(err); return; } resolve(); }); }); } // エントリを追加 function addEntry(client, dn, attrs) { return new Promise(function (resolve, reject) { client.add(dn, attrs, function (err) { if (err) { reject(err); return; } resolve(); }); }); } // エントリを削除 function deleteEntry(client, dn) { return new Promise(function (resolve, reject) { client.del(dn, function (err) { if (err) { reject(err); return; } resolve(); }); }); } // CSV ファイルを読み込み行配列を返す function readCsvRows(filePath) { return new Promise(function (resolve, reject) { const rows = []; const stream = fs.createReadStream(filePath) .pipe(iconv.decodeStream('Shift_JIS')) // 読み込み時に SJIS → UTF-8 へ変換 .pipe(csv()); stream.on('data', function (row) { rows.push(row); }); stream.on('end', function () { resolve(rows); }); stream.on('error', function (err) { reject(err); }); }); } // 更新行から LDAP modify 用の Change 配列を生成 function buildChangesFromUpdate(update) { const changes = []; const keys = Object.keys(update); for (let i = 0; i < keys.length; i += 1) { const attr = keys[i]; // dn/objectClass は除外し、RDN となる uid も modify 対象から外す if (attr !== 'dn' && attr !== 'objectClass' && attr !== 'uid' && update[attr]) { const values = Array.isArray(update[attr]) ? update[attr] : [String(update[attr])]; const modification = new ldap.Attribute({ type: attr, values: values }); changes.push(new ldap.Change({ operation: 'replace', modification: modification })); } } return changes; } // searchEntry から DN を抽出 function getDnFromSearchEntry(entry) { if (!entry) { return null; } return entry.objectName || entry.dn || (entry.object && entry.object.dn) || null; } // LDAP エントリを新規追加 async function addLdapEntry(client, entry) { const dn = entry.dn || `uid=${entry.uid},cn=users,${BASE_DN}`; const objectClass = entry.objectClass ? entry.objectClass.split(',') : [ 'top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount' ]; const attrs = Object.assign({}, entry, { objectClass: objectClass }); delete attrs.dn; await addEntry(client, dn, attrs); console.log(`LDAPエントリ新規登録: ${dn}`); } // uidNumber で更新 or なければ新規追加 async function upsertEntry(client, update) { const uidNumber = update.uidNumber; if (uidNumber) { const results = await searchEntries(client, { scope: 'sub', filter: `(uidNumber=${uidNumber})`, attributes: ['dn'] }); if (results.length > 0) { const dn = getDnFromSearchEntry(results[0]); if (!dn) { console.error('dnが取得できませんでした:', results[0]); return; } const changes = buildChangesFromUpdate(update); if (changes.length > 0) { await modifyEntry(client, dn, changes); console.log(`LDAPエントリを更新しました: ${dn}`); } else { console.log(`変更対象属性がありません: ${dn}`); } } else { await addLdapEntry(client, update); } } else { await addLdapEntry(client, update); } } // LDAP から取得し Shift-JIS CSV へエクスポート async function exportLdapToCsv() { let client; try { client = await createBoundClient(); } catch (bindErr) { console.error('LDAP bind error:', bindErr); return; } try { const rawEntries = await searchEntries(client, { scope: 'sub', filter: 'objectClass=person', attributes: [] }); const entries = rawEntries .map(function (entry) { return entryToObject(entry); }) .filter(function (item) { return item !== null; }); if (entries.length === 0) { console.log('エントリが見つかりませんでした。'); return; } const normalized = entries.map(function (obj) { const normalizedObj = {}; if (obj && typeof obj === 'object') { const keys = Object.keys(obj); for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; if (Array.isArray(obj[key])) { normalizedObj[key] = obj[key].join(','); } else { normalizedObj[key] = obj[key]; } } } return normalizedObj; }); const allFields = Array.from(normalized.reduce(function (set, obj) { if (obj && typeof obj === 'object') { const keys = Object.keys(obj); for (let i = 0; i < keys.length; i += 1) { set.add(keys[i]); } } return set; }, new Set())); const parser = new Parser({ fields: allFields }); const csvData = parser.parse(normalized); const sjisData = iconv.encode(csvData, 'Shift_JIS'); fs.writeFileSync(EXPORT_CSV, sjisData); console.log(`LDAPデータをShift-JISでCSVにエクスポートしました: ${EXPORT_CSV}`); } catch (err) { console.error('LDAP search/export error:', err); } finally { if (client) { client.unbind(); } } } // CSV を読み込み LDAP を更新(逐次 upsert) async function importCsvToLdap() { let updates; try { updates = await readCsvRows(IMPORT_CSV); } catch (readErr) { console.error('CSV読込エラー:', readErr); return; } if (!updates || updates.length === 0) { console.log('CSVに更新対象データがありません。'); return; } let client; try { client = await createBoundClient(); } catch (bindErr) { console.error('LDAP bind error:', bindErr); return; } try { for (let i = 0; i < updates.length; i += 1) { await upsertEntry(client, updates[i]); } } catch (err) { console.error('LDAP import error:', err); } finally { if (client) { client.unbind(); } } } // 指定 uidNumber のエントリを削除 async function deleteEntriesByUidNumbers(uidNumbers) { if (!Array.isArray(uidNumbers) || uidNumbers.length === 0) { console.log('uidNumberの配列を指定してください。'); return; } let client; try { client = await createBoundClient(); } catch (bindErr) { console.error('LDAP bind error:', bindErr); return; } try { for (let i = 0; i < uidNumbers.length; i += 1) { const uidNumber = uidNumbers[i]; const results = await searchEntries(client, { scope: 'sub', filter: `(uidNumber=${uidNumber})`, attributes: ['dn'] }); if (results.length === 0) { console.log(`uidNumber=${uidNumber} のエントリーが見つかりませんでした。`); continue; } const dn = getDnFromSearchEntry(results[0]); if (!dn) { console.error('dnが取得できませんでした:', results[0]); continue; } await deleteEntry(client, dn); console.log(`削除成功: ${dn}`); } } catch (err) { console.error('LDAP delete error:', err); } finally { if (client) { client.unbind(); } } } // コマンドライン引数に応じて処理を振り分け async function main() { const mode = process.argv[2]; if (mode === 'export') { await exportLdapToCsv(); } else if (mode === 'import') { await importCsvToLdap(); } else if (mode === 'delete-uids') { const uidNumbers = (process.argv[3] || '').split(',').map(function (s) { return s.trim(); }).filter(function (v) { return Boolean(v); }); await deleteEntriesByUidNumbers(uidNumbers); } else { console.log('使い方:'); console.log(' node ldapUpdate.js export # cn=users配下のLDAPデータをCSVエクスポート'); console.log(' node ldapUpdate.js import # CSVからLDAPを更新'); console.log(' node ldapUpdate.js delete-uids 1000003,1000004 # 指定uidNumberのエントリーを削除'); } } main();