734 lines
24 KiB
JavaScript
734 lines
24 KiB
JavaScript
'use strict';
|
||
|
||
const fs = require('fs');
|
||
require('./patch-ldap-dn');
|
||
const ldap = require('ldapjs');
|
||
const config = require('./config');
|
||
const passwordSetter = require('./passwordSetter');
|
||
|
||
function urlScheme(url) {
|
||
const match = /^([a-z0-9+.-]+):\/\//i.exec(String(url || ''));
|
||
return match ? match[1].toLowerCase() : '';
|
||
}
|
||
|
||
class LdapError extends Error {
|
||
constructor(message, code, status) {
|
||
super(message);
|
||
this.name = 'LdapError';
|
||
this.ldapCode = code;
|
||
this.status = status || 500;
|
||
}
|
||
}
|
||
|
||
function mapLdapError(err) {
|
||
if (!err) return err;
|
||
const code = err.code;
|
||
switch (code) {
|
||
case 49: return new LdapError('Неверный логин или пароль', code, 401);
|
||
case 68: return new LdapError('Запись уже существует', code, 409);
|
||
case 32: return new LdapError('Объект не найден', code, 404);
|
||
case 50: return new LdapError('Недостаточно прав для выполнения операции', code, 403);
|
||
case 53: return new LdapError('Операция запрещена сервером', code, 500);
|
||
case 19: return new LdapError('Нарушение ограничений (например, слишком короткий пароль)', code, 400);
|
||
case 21: return new LdapError('Недопустимое значение атрибута (проверьте ФИО, телефон, почту)', code, 400);
|
||
case 14: return new LdapError('Операция недоступна', code, 500);
|
||
case 52: return new LdapError('Сервер недоступен', code, 500);
|
||
case 80: return new LdapError('Не удалось подключиться к LDAP-серверу', code, 502);
|
||
default: return new LdapError(err.message || 'Ошибка LDAP', code, 500);
|
||
}
|
||
}
|
||
|
||
function unescapeDN(value) {
|
||
return String(value).replace(/\\[0-9a-fA-F]{2}/g, (m) => String.fromCharCode(parseInt(m.slice(1), 16)));
|
||
}
|
||
|
||
function getRDNName(dn) {
|
||
if (!dn) return '';
|
||
const first = dn.split(',').shift();
|
||
if (!first) return '';
|
||
const eq = first.indexOf('=');
|
||
return eq === -1 ? unescapeDN(first) : unescapeDN(first.slice(eq + 1));
|
||
}
|
||
|
||
function getCN(dn) {
|
||
const name = getRDNName(dn);
|
||
return name;
|
||
}
|
||
|
||
function escapeDNValue(value) {
|
||
return String(value).replace(/([,\\#+<>;"=])/g, '\\$1');
|
||
}
|
||
|
||
function escapeFilter(value) {
|
||
return String(value).replace(/[\\*()\0]/g, (ch) => '\\' + ch.charCodeAt(0).toString(16).padStart(2, '0'));
|
||
}
|
||
|
||
function buildBindNames(login) {
|
||
const names = [];
|
||
const netbios = config.domain.split('.')[0].toUpperCase();
|
||
if (login.includes('@') || login.startsWith('CN=') || login.startsWith('OU=') || login.includes(',')) {
|
||
names.push(login);
|
||
} else {
|
||
names.push(`${login}@${config.domain}`);
|
||
names.push(`${netbios}\\${login}`);
|
||
names.push(login);
|
||
}
|
||
return names;
|
||
}
|
||
|
||
async function connect() {
|
||
const scheme = urlScheme(config.ldapUrl);
|
||
const clientOpts = {
|
||
url: config.ldapUrl,
|
||
connectTimeout: 10000,
|
||
timeout: 15000,
|
||
reconnect: false
|
||
};
|
||
|
||
if (scheme === 'ldaps' || scheme === 'ldap') {
|
||
const tlsOptions = { rejectUnauthorized: config.tlsRejectUnauthorized };
|
||
if (config.caFile) {
|
||
try {
|
||
tlsOptions.ca = [fs.readFileSync(config.caFile)];
|
||
} catch (err) {
|
||
throw new LdapError(`Не удалось прочитать файл CA: ${config.caFile}: ${err.message}`, 0, 502);
|
||
}
|
||
}
|
||
clientOpts.tlsOptions = tlsOptions;
|
||
}
|
||
|
||
const client = ldap.createClient(clientOpts);
|
||
|
||
await new Promise((resolve, reject) => {
|
||
let settled = false;
|
||
const timer = setTimeout(() => {
|
||
if (settled) return;
|
||
settled = true;
|
||
reject(new LdapError('Не удалось подключиться к LDAP-серверу (таймаут)', 0, 502));
|
||
try { client.destroy(); } catch (e) {}
|
||
}, 15000);
|
||
|
||
client.once('connect', () => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
resolve();
|
||
});
|
||
client.on('error', (err) => {
|
||
if (settled) return;
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
reject(new LdapError(`Ошибка подключения к LDAP-серверу: ${err.message || err.code}`, 0, 502));
|
||
try { client.destroy(); } catch (e) {}
|
||
});
|
||
});
|
||
|
||
client.on('error', () => {});
|
||
|
||
if (config.encryptTraffic && scheme === 'ldap') {
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
client.starttls({}, null, (err) => (err ? reject(err) : resolve()));
|
||
});
|
||
} catch (err) {
|
||
if (err && err.code === 52) {
|
||
throw new LdapError(
|
||
'StartTLS отклонён сервером (код 52): на контроллере домена не настроен LDAPS/TLS. ' +
|
||
'Включите LDAPS на DC или установите ENCRYPT_TRAFFIC=false',
|
||
0,
|
||
502
|
||
);
|
||
}
|
||
throw new LdapError(`StartTLS не удался (проверьте LDAP_URL или ENCRYPT_TRAFFIC): ${err.message || err.code}`, 0, 502);
|
||
}
|
||
}
|
||
return client;
|
||
}
|
||
|
||
async function bind(client, login, password) {
|
||
const names = buildBindNames(login);
|
||
let lastErr = null;
|
||
for (const name of names) {
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
client.bind(name, password, (err) => (err ? reject(err) : resolve()));
|
||
});
|
||
return name;
|
||
} catch (err) {
|
||
lastErr = err;
|
||
}
|
||
}
|
||
throw mapLdapError(lastErr);
|
||
}
|
||
|
||
async function close(client) {
|
||
return new Promise((resolve) => {
|
||
if (!client || client.destroyed) return resolve();
|
||
try {
|
||
client.unbind((err) => {
|
||
try { client.destroy(); } catch (e) {}
|
||
resolve();
|
||
});
|
||
} catch (err) {
|
||
try { client.destroy(); } catch (e) {}
|
||
resolve();
|
||
}
|
||
});
|
||
}
|
||
|
||
function searchEntries(client, base, options) {
|
||
return new Promise((resolve, reject) => {
|
||
const entries = [];
|
||
client.search(base, options, (err, res) => {
|
||
if (err) return reject(mapLdapError(err));
|
||
res.on('searchEntry', (entry) => entries.push(entry));
|
||
res.on('error', (e) => reject(mapLdapError(e)));
|
||
res.on('end', (result) => {
|
||
if (result && result.status !== 0) {
|
||
return reject(new LdapError(`Ошибка поиска LDAP (статус ${result.status})`, result.status, 500));
|
||
}
|
||
resolve(entries);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function entryToObject(entry) {
|
||
const obj = {};
|
||
const dn = entry.dn ? entry.dn.toString() : String(entry.objectName || '');
|
||
if (dn) obj.dn = dn;
|
||
const alwaysArray = new Set(['memberOf', 'objectClass', 'proxyAddresses']);
|
||
for (const attr of entry.attributes || []) {
|
||
const vals = attr.values || [];
|
||
if (alwaysArray.has(attr.type) || vals.length > 1) {
|
||
obj[attr.type] = vals;
|
||
} else {
|
||
obj[attr.type] = vals[0];
|
||
}
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
function addEntry(client, dn, attributes) {
|
||
return new Promise((resolve, reject) => {
|
||
const entry = {};
|
||
for (const a of attributes || []) {
|
||
entry[a.attr] = a.vals;
|
||
}
|
||
client.add(dn, entry, (err) => (err ? reject(mapLdapError(err)) : resolve()));
|
||
});
|
||
}
|
||
|
||
function normalizeChange(change) {
|
||
const mod = change.modification || {};
|
||
const key = Object.keys(mod)[0];
|
||
const values = mod[key];
|
||
return {
|
||
operation: change.operation,
|
||
modification: {
|
||
type: key,
|
||
values: Array.isArray(values) ? values : [values]
|
||
}
|
||
};
|
||
}
|
||
|
||
function modifyEntry(client, dn, changes) {
|
||
return new Promise((resolve, reject) => {
|
||
client.modify(dn, (changes || []).map(normalizeChange), (err) => (err ? reject(mapLdapError(err)) : resolve()));
|
||
});
|
||
}
|
||
|
||
async function setPassword(client, userDN, password) {
|
||
switch (config.passwordMethod) {
|
||
case 'powershell':
|
||
await passwordSetter.setPasswordViaPowerShell(userDN, password, { exe: config.powerShellExe, ldapUrl: config.ldapUrl });
|
||
return;
|
||
case 'kerberos': {
|
||
const principal = passwordSetter.buildPrincipal(config.adminLogin, config.ldapRealm);
|
||
await passwordSetter.setPasswordViaKerberos(userDN, password, {
|
||
ldapUrl: config.ldapUrl,
|
||
principal,
|
||
adminPassword: config.adminPassword,
|
||
retries: config.passwordRetries,
|
||
retryDelayMs: config.passwordRetryDelayMs
|
||
});
|
||
return;
|
||
}
|
||
case 'ldap':
|
||
default:
|
||
break;
|
||
}
|
||
const value = Buffer.from('"' + password + '"', 'utf16le');
|
||
await modifyEntry(client, userDN, [{ operation: 'replace', modification: { unicodePwd: [value] } }]);
|
||
}
|
||
|
||
function removeEntry(client, dn) {
|
||
return new Promise((resolve, reject) => {
|
||
client.del(dn, (err) => (err ? reject(mapLdapError(err)) : resolve()));
|
||
});
|
||
}
|
||
|
||
// ================= Public auth (port of the Go service) =================
|
||
|
||
async function authenticate(username, password, mode) {
|
||
if (!username) throw new LdapError('Username is required', 0, 400);
|
||
if (!password) throw new LdapError('Password is required', 0, 400);
|
||
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, username, password);
|
||
|
||
// Rebind as admin to read user info (like the Go service did with the service account)
|
||
if (config.adminLogin && config.adminPassword) {
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
} catch (err) {
|
||
console.warn(
|
||
`⚠ Административная привязка не удалась (ADMIN_LOGIN/ADMIN_PASSWORD): ${err.message || err.code}. ` +
|
||
`Проверьте учётные данные в .env (пароли с '#' нужно брать в кавычки). Возвращаются базовые данные.`
|
||
);
|
||
return { success: true, username };
|
||
}
|
||
}
|
||
|
||
return await getUserInfo(client, username, mode);
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
async function getUserInfo(client, username, mode) {
|
||
let attributes = ['dn', 'sAMAccountName'];
|
||
|
||
if (mode !== 'groups' && mode !== 'description') {
|
||
attributes.push('memberOf', 'mail', 'displayName', 'cn', 'description');
|
||
}
|
||
if (mode === 'description') attributes.push('description');
|
||
if (mode === 'groups') attributes.push('memberOf');
|
||
if (mode === 'all') attributes = ['*', '+'];
|
||
|
||
const entries = await searchEntries(client, config.baseDN, {
|
||
scope: 'sub',
|
||
filter: `(sAMAccountName=${escapeFilter(username)})`,
|
||
attributes
|
||
});
|
||
|
||
if (entries.length === 0) throw new LdapError('User not found', 0, 404);
|
||
const entry = entryToObject(entries[0]);
|
||
|
||
if (mode === 'description') {
|
||
return { success: true, description: entry.description || '' };
|
||
}
|
||
|
||
if (mode === 'groups') {
|
||
return { success: true, groups: (entry.memberOf || []).map(getCN).filter(Boolean) };
|
||
}
|
||
|
||
const result = {
|
||
success: true,
|
||
username: entry.sAMAccountName || username,
|
||
full_name: entry.displayName || entry.cn || username,
|
||
email: entry.mail || '',
|
||
description: entry.description || '',
|
||
groups: (entry.memberOf || []).map(getCN).filter(Boolean)
|
||
};
|
||
|
||
if (mode === 'all') {
|
||
result.all_info = entry;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ================= Admin: listing =================
|
||
|
||
async function listOUs() {
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
const entries = await searchEntries(client, config.baseDN, {
|
||
scope: 'sub',
|
||
filter: '(objectClass=organizationalUnit)',
|
||
attributes: ['dn']
|
||
});
|
||
return entries.map((e) => {
|
||
const obj = entryToObject(e);
|
||
return { dn: obj.dn, name: getRDNName(obj.dn) };
|
||
});
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
async function listGroups() {
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
const base = config.groupOU || config.baseDN;
|
||
const entries = await searchEntries(client, base, {
|
||
scope: 'sub',
|
||
filter: '(objectClass=group)',
|
||
attributes: ['dn', 'cn']
|
||
});
|
||
return entries.map((e) => {
|
||
const obj = entryToObject(e);
|
||
return { dn: obj.dn, cn: obj.cn || getCN(obj.dn) };
|
||
});
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
function parseUser(entry) {
|
||
const uac = parseInt(entry.userAccountControl, 10) || 0;
|
||
return {
|
||
dn: entry.dn,
|
||
login: entry.sAMAccountName || '',
|
||
full_name: entry.displayName || entry.cn || '',
|
||
given_name: entry.givenName || '',
|
||
sn: entry.sn || '',
|
||
middle_name: entry.middleName || '',
|
||
email: entry.mail || '',
|
||
phone: entry.telephoneNumber || '',
|
||
position: entry.description || '',
|
||
upn: entry.userPrincipalName || '',
|
||
disabled: (uac & 0x0002) !== 0,
|
||
groups: (entry.memberOf || []).map(getCN).filter(Boolean),
|
||
member_of: entry.memberOf || []
|
||
};
|
||
}
|
||
|
||
async function listUsers() {
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
const entries = await searchEntries(client, config.baseDN, {
|
||
scope: 'sub',
|
||
filter: '(&(objectClass=user)(objectCategory=person))',
|
||
attributes: ['dn', 'sAMAccountName', 'displayName', 'cn', 'givenName', 'sn', 'middleName',
|
||
'mail', 'telephoneNumber', 'description', 'userPrincipalName', 'userAccountControl', 'memberOf']
|
||
});
|
||
return entries.map((e) => parseUser(entryToObject(e)));
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
async function findUserByLogin(client, login) {
|
||
const entries = await searchEntries(client, config.baseDN, {
|
||
scope: 'sub',
|
||
filter: `(sAMAccountName=${escapeFilter(login)})`,
|
||
attributes: ['*']
|
||
});
|
||
if (entries.length === 0) throw new LdapError(`Пользователь "${login}" не найден`, 0, 404);
|
||
return entryToObject(entries[0]);
|
||
}
|
||
|
||
async function getUser(login) {
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
const entry = await findUserByLogin(client, login);
|
||
const user = parseUser(entry);
|
||
user.ou = getParentDN(entry.dn);
|
||
return user;
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
function getParentDN(dn) {
|
||
const parts = dn.split(',');
|
||
parts.shift();
|
||
return parts.join(',');
|
||
}
|
||
|
||
async function resolveGroupDNs(client, groupCNs) {
|
||
const dns = [];
|
||
for (const cn of groupCNs || []) {
|
||
const entries = await searchEntries(client, config.groupOU || config.baseDN, {
|
||
scope: 'sub',
|
||
filter: `(cn=${escapeFilter(cn)})`,
|
||
attributes: ['dn', 'primaryGroupToken']
|
||
});
|
||
if (entries.length > 0) {
|
||
const obj = entryToObject(entries[0]);
|
||
dns.push({ dn: obj.dn, primaryGroupToken: obj.primaryGroupToken });
|
||
} else {
|
||
throw new LdapError(`Группа "${cn}" не найдена`, 0, 400);
|
||
}
|
||
}
|
||
return dns;
|
||
}
|
||
|
||
// ================= Admin: create / update / block =================
|
||
|
||
async function findFreeCn(client, ouDn, name) {
|
||
const base = String(name || '').trim();
|
||
const entries = await searchEntries(client, ouDn || config.baseDN, {
|
||
scope: 'one',
|
||
filter: '(objectClass=*)',
|
||
attributes: ['cn']
|
||
});
|
||
const taken = new Set(entries.map((e) => String(entryToObject(e).cn || '').toLowerCase()));
|
||
let candidate = base;
|
||
let n = 1;
|
||
while (taken.has(candidate.toLowerCase())) {
|
||
n += 1;
|
||
candidate = `${base} ${n}`;
|
||
}
|
||
return candidate;
|
||
}
|
||
|
||
async function createUser({ ouDn, fio, login, password, email, phone, position, logonScript, homeDrive, homeBasePath, groupCNs }) {
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
|
||
const existing = await searchEntries(client, config.baseDN, {
|
||
scope: 'sub',
|
||
filter: `(sAMAccountName=${escapeFilter(login)})`,
|
||
attributes: ['dn']
|
||
});
|
||
if (existing.length > 0) {
|
||
throw new LdapError(`Логин "${login}" уже занят`, 0, 409);
|
||
}
|
||
|
||
const upn = `${login}@${config.domain}`;
|
||
const upnExisting = await searchEntries(client, config.baseDN, {
|
||
scope: 'sub',
|
||
filter: `(userPrincipalName=${escapeFilter(upn)})`,
|
||
attributes: ['dn']
|
||
});
|
||
if (upnExisting.length > 0) {
|
||
throw new LdapError(`Логин "${login}" уже занят`, 0, 409);
|
||
}
|
||
|
||
const cn = escapeDNValue(await findFreeCn(client, ouDn, fio));
|
||
const userDN = `CN=${cn},${ouDn || config.baseDN}`;
|
||
|
||
const attributes = [
|
||
{ attr: 'objectClass', vals: ['top', 'person', 'organizationalPerson', 'user'] },
|
||
{ attr: 'sAMAccountName', vals: [login] },
|
||
{ attr: 'userPrincipalName', vals: [`${login}@${config.domain}`] },
|
||
{ attr: 'displayName', vals: [fio] },
|
||
{ attr: 'userAccountControl', vals: ['512'] }
|
||
];
|
||
|
||
const nameParts = (fio || '').trim().split(/\s+/);
|
||
if (nameParts[0]) attributes.push({ attr: 'givenName', vals: [nameParts[0]] });
|
||
if (nameParts[1]) attributes.push({ attr: 'sn', vals: [nameParts.slice(1).join(' ')] });
|
||
if (email) attributes.push({ attr: 'mail', vals: [email] });
|
||
if (phone) attributes.push({ attr: 'telephoneNumber', vals: [phone] });
|
||
if (position) {
|
||
const positionVal = String(position).trim();
|
||
attributes.push({ attr: 'description', vals: [positionVal] });
|
||
attributes.push({ attr: 'title', vals: [positionVal] });
|
||
}
|
||
if (logonScript) attributes.push({ attr: 'scriptPath', vals: [String(logonScript).trim()] });
|
||
if (homeDrive && homeBasePath) {
|
||
attributes.push({ attr: 'homeDrive', vals: [String(homeDrive).trim()] });
|
||
attributes.push({ attr: 'homeDirectory', vals: [`${String(homeBasePath).replace(/\\+$/, '')}\\${login}`] });
|
||
}
|
||
|
||
await addEntry(client, userDN, attributes);
|
||
|
||
try {
|
||
if (password) {
|
||
await setPassword(client, userDN, password);
|
||
}
|
||
|
||
const groupDNs = await resolveGroupDNs(client, groupCNs);
|
||
const created = await findUserByLogin(client, login);
|
||
const memberDNs = new Set((created.memberOf || []).map((d) => String(d).toLowerCase()));
|
||
const primaryGroupId = String(created.primaryGroupID || '').trim();
|
||
for (const group of groupDNs) {
|
||
if (String(group.primaryGroupToken || '').trim() === primaryGroupId) continue;
|
||
if (memberDNs.has(String(group.dn).toLowerCase())) continue;
|
||
await modifyEntry(client, group.dn, [{ operation: 'add', modification: { member: [userDN] } }]);
|
||
}
|
||
} catch (err) {
|
||
try {
|
||
await removeEntry(client, userDN);
|
||
} catch (_) {
|
||
// best-effort rollback: keep the original error
|
||
}
|
||
throw err;
|
||
}
|
||
|
||
return { success: true, dn: userDN, login };
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
function cleanValue(value, maxLen) {
|
||
return String(value || '')
|
||
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, maxLen || 200);
|
||
}
|
||
|
||
function splitFioParts(fio) {
|
||
const parts = cleanValue(fio).split(' ');
|
||
return {
|
||
lastName: parts[0] || '',
|
||
firstName: parts[1] || '',
|
||
middleName: parts.slice(2).join(' ') || ''
|
||
};
|
||
}
|
||
|
||
async function updateUser(login, { fio, lastName, firstName, middleName, email, phone, position, logonScript, homeDrive, homeBasePath, password, groupCNs }) {
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
|
||
const entry = await findUserByLogin(client, login);
|
||
const userDN = entry.dn;
|
||
const changes = [];
|
||
|
||
const hasNameFields = lastName !== undefined || firstName !== undefined || middleName !== undefined;
|
||
if (fio !== undefined || hasNameFields) {
|
||
let lname = lastName !== undefined ? cleanValue(lastName, 64) : null;
|
||
let fname = firstName !== undefined ? cleanValue(firstName, 64) : null;
|
||
let mname = middleName !== undefined ? cleanValue(middleName, 64) : null;
|
||
if (fio !== undefined && !hasNameFields) {
|
||
const parts = splitFioParts(fio);
|
||
if (lname === null) lname = parts.lastName;
|
||
if (fname === null) fname = parts.firstName;
|
||
if (mname === null) mname = parts.middleName;
|
||
}
|
||
const display = [lname, fname, mname].filter(Boolean).join(' ');
|
||
if (display) {
|
||
changes.push({ operation: 'replace', modification: { displayName: [display] } });
|
||
}
|
||
if (lname !== null && lname) {
|
||
changes.push({ operation: 'replace', modification: { sn: [lname] } });
|
||
}
|
||
if (fname !== null && fname) {
|
||
changes.push({ operation: 'replace', modification: { givenName: [fname] } });
|
||
}
|
||
if (mname !== null && mname) {
|
||
changes.push({ operation: 'replace', modification: { middleName: [mname] } });
|
||
}
|
||
}
|
||
if (email !== undefined) {
|
||
const emailVal = cleanValue(email, 254);
|
||
if (emailVal) {
|
||
changes.push({ operation: 'replace', modification: { mail: [emailVal] } });
|
||
}
|
||
}
|
||
if (phone !== undefined) {
|
||
changes.push({ operation: 'replace', modification: { telephoneNumber: [cleanValue(phone, 60)] } });
|
||
}
|
||
if (position !== undefined) {
|
||
const positionVal = cleanValue(position, 120);
|
||
changes.push({ operation: 'replace', modification: { description: [positionVal] } });
|
||
changes.push({ operation: 'replace', modification: { title: [positionVal] } });
|
||
}
|
||
if (logonScript !== undefined) {
|
||
changes.push({ operation: 'replace', modification: { scriptPath: [cleanValue(logonScript, 255)] } });
|
||
}
|
||
if (homeDrive !== undefined) {
|
||
changes.push({ operation: 'replace', modification: { homeDrive: [cleanValue(homeDrive, 3)] } });
|
||
}
|
||
if (homeBasePath !== undefined) {
|
||
changes.push({
|
||
operation: 'replace',
|
||
modification: {
|
||
homeDirectory: homeBasePath.trim()
|
||
? [`${String(homeBasePath).trim().replace(/\\+$/, '')}\\${login}`]
|
||
: ['']
|
||
}
|
||
});
|
||
}
|
||
|
||
if (changes.length > 0) {
|
||
await modifyEntry(client, userDN, changes);
|
||
}
|
||
|
||
if (password) {
|
||
await setPassword(client, userDN, password);
|
||
}
|
||
|
||
if (groupCNs !== undefined) {
|
||
const currentGroupDNs = (entry.memberOf || []).map((d) => d.toLowerCase());
|
||
const primaryGroupId = String(entry.primaryGroupID || '').trim();
|
||
const targetGroupDNs = await resolveGroupDNs(client, groupCNs);
|
||
|
||
const toAdd = targetGroupDNs.filter(
|
||
(g) =>
|
||
String(g.primaryGroupToken || '').trim() !== primaryGroupId &&
|
||
!currentGroupDNs.includes(g.dn.toLowerCase())
|
||
);
|
||
const toRemove = currentGroupDNs.filter(
|
||
(d) => !targetGroupDNs.some((t) => t.dn.toLowerCase() === d)
|
||
);
|
||
|
||
for (const group of toAdd) {
|
||
await modifyEntry(client, group.dn, [{ operation: 'add', modification: { member: [userDN] } }]);
|
||
}
|
||
for (const groupDN of toRemove) {
|
||
await modifyEntry(client, groupDN, [{ operation: 'delete', modification: { member: [userDN] } }]);
|
||
}
|
||
}
|
||
|
||
return { success: true, dn: userDN, login };
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
async function setUserEnabled(login, enabled) {
|
||
const client = await connect();
|
||
try {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
|
||
const entry = await findUserByLogin(client, login);
|
||
const userDN = entry.dn;
|
||
const uac = parseInt(entry.userAccountControl, 10) || 0;
|
||
const newUac = enabled ? (uac & ~0x0002) : (uac | 0x0002);
|
||
|
||
await modifyEntry(client, userDN, [
|
||
{ operation: 'replace', modification: { userAccountControl: [String(newUac)] } }
|
||
]);
|
||
|
||
return { success: true, dn: userDN, login, enabled };
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
async function testConnection() {
|
||
const client = await connect();
|
||
try {
|
||
if (config.adminLogin && config.adminPassword) {
|
||
await bind(client, config.adminLogin, config.adminPassword);
|
||
}
|
||
await searchEntries(client, config.baseDN, {
|
||
scope: 'base',
|
||
filter: '(objectClass=*)',
|
||
attributes: ['distinguishedName']
|
||
});
|
||
return true;
|
||
} finally {
|
||
await close(client);
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
LdapError,
|
||
authenticate,
|
||
getUserInfo,
|
||
listOUs,
|
||
listGroups,
|
||
listUsers,
|
||
getUser,
|
||
createUser,
|
||
updateUser,
|
||
setUserEnabled,
|
||
testConnection,
|
||
buildBindNames,
|
||
getCN,
|
||
escapeFilter
|
||
}; |