90 lines
3.5 KiB
JavaScript
90 lines
3.5 KiB
JavaScript
'use strict';
|
||
|
||
const { spawn } = require('child_process');
|
||
|
||
// Sets a user's password via PowerShell/ADSI (IADsUser.SetPassword).
|
||
// On a domain-joined machine ADSI negotiates a Kerberos signed/sealed
|
||
// connection, which Active Directory accepts as a "protected channel"
|
||
// for unicodePwd — unlike a plain LDAP bind over TCP (AD error 53).
|
||
//
|
||
// The DN and password are passed via environment variables so they never
|
||
// appear in the process command line, and the script body is pure ASCII
|
||
// (no encoding issues with Windows PowerShell 5.1). The script itself is
|
||
// sent over stdin to avoid temp files.
|
||
//
|
||
// The server is passed explicitly (when `ldapUrl` is provided) so ADSI
|
||
// targets the SAME domain controller that the LDAP write went to. Without
|
||
// it ADSI does a serverless bind and the DC Locator may pick another DC
|
||
// (e.g. on a remote site) that has not replicated the freshly created user
|
||
// yet, which fails with "Такой объект на сервере отсутствует" (0x8007203A).
|
||
|
||
function adsiServerPrefix(ldapUrl) {
|
||
const match = /^([a-z0-9+.-]+):\/\/([^/:]+)(?::(\d+))?/i.exec(String(ldapUrl || '').trim());
|
||
if (!match) return '';
|
||
const scheme = match[1].toLowerCase();
|
||
const host = match[2];
|
||
const port = match[3];
|
||
const prefix = scheme === 'ldaps' ? 'LDAPS://' : 'LDAP://';
|
||
return prefix + host + (port ? ':' + port : '') + '/';
|
||
}
|
||
|
||
function setPasswordViaPowerShell(userDN, password, { exe = 'powershell.exe', timeoutMs = 15000, ldapUrl = '' } = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const serverPrefix = adsiServerPrefix(ldapUrl);
|
||
const script = [
|
||
'$ErrorActionPreference = "Stop"',
|
||
'$dn = [Environment]::GetEnvironmentVariable("LDAP_PW_DN")',
|
||
'$pw = [Environment]::GetEnvironmentVariable("LDAP_PW_VALUE")',
|
||
'$server = [Environment]::GetEnvironmentVariable("LDAP_PW_SERVER")',
|
||
'$target = if ($server) { $server + $dn } else { "LDAP://" + $dn }',
|
||
'$user = [adsi]($target)',
|
||
'$user.SetPassword($pw)'
|
||
].join('; ');
|
||
|
||
let child;
|
||
try {
|
||
child = spawn(exe, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', '-'], {
|
||
env: { ...process.env, LDAP_PW_DN: userDN, LDAP_PW_VALUE: password, LDAP_PW_SERVER: serverPrefix },
|
||
windowsHide: true,
|
||
stdio: ['pipe', 'pipe', 'pipe']
|
||
});
|
||
} catch (err) {
|
||
reject(new Error(`Не удалось запустить PowerShell для установки пароля: ${err.message}`));
|
||
return;
|
||
}
|
||
|
||
let stdout = '';
|
||
let stderr = '';
|
||
let timedOut = false;
|
||
const timer = setTimeout(() => {
|
||
timedOut = true;
|
||
try { child.kill(); } catch (_) { /* ignore */ }
|
||
}, timeoutMs);
|
||
|
||
child.stdout.on('data', (d) => { stdout += d; });
|
||
child.stderr.on('data', (d) => { stderr += d; });
|
||
|
||
child.on('error', (err) => {
|
||
clearTimeout(timer);
|
||
reject(new Error(`Не удалось запустить PowerShell для установки пароля: ${err.message}`));
|
||
});
|
||
|
||
child.on('close', (code) => {
|
||
clearTimeout(timer);
|
||
if (timedOut) {
|
||
reject(new Error('Таймаут при установке пароля через PowerShell'));
|
||
return;
|
||
}
|
||
if (code === 0) {
|
||
resolve();
|
||
return;
|
||
}
|
||
const detail = String(stderr || stdout || '').trim();
|
||
reject(new Error(`Ошибка установки пароля через PowerShell (код ${code})${detail ? ': ' + detail : ''}`));
|
||
});
|
||
|
||
child.stdin.end(script);
|
||
});
|
||
}
|
||
|
||
module.exports = setPasswordViaPowerShell; |