370 lines
12 KiB
JavaScript
370 lines
12 KiB
JavaScript
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(); |