Objavljen izvor
Preuzmi ZIP izvora
CH-J Server Manager
Pregledaj direktorije i datoteke za određeni programski izdanje.
CH-J / Server Manager
377 datoteke
core-20261019T111616Z-160a00bfcddc
Dodano: · Izgrađeno:
1
"use strict";3
// Destructive end-to-end tests of chj.users against an ISOLATED Windows test machine (never production).4
// Creates temporary local accounts "chjtst*", changes sshd_config through CH-J (restarting sshd) and restores5
// the original sshd_config at the end. Credentials come only from the environment:6
// CHJ_USERS_WINDOWS_HOST=192.0.2.5 CHJ_USERS_WINDOWS_HOST6=2001:db8::5 CHJ_USERS_WINDOWS_USER=admin7
// CHJ_USERS_WINDOWS_PASSWORD=... CHJ_USERS_WINDOWS_FINGERPRINT=<base64 SHA256 host key> node --test test/usersWindowsIntegration.test.js8
// Optional: CHJ_USERS_WINDOWS_SLOW=1 also runs the 2-minute rollback-guard scenarios.9
const test = require("node:test");10
const assert = require("node:assert/strict");11
const crypto = require("node:crypto");12
const { Client } = require("ssh2");13
const { SessionManager } = require("../src/main/sessions/sessionManager");14
const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");15
const { WindowsExecutor, encodedCommand } = require("../src/main/sessions/windowsPrivilegedExec");16
const { detectPlatform } = require("../src/main/sessions/platformDetect");17
const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService");18
const { WindowsUserAdminService, REMOTE_SCRIPT } = require("../src/main/users/windowsUserAdminService");19
const { KeyGeneratorService } = require("../src/main/keys/keyGeneratorService");21
const env = process.env;22
const enabled = Boolean(env.CHJ_USERS_WINDOWS_HOST && env.CHJ_USERS_WINDOWS_PASSWORD && env.CHJ_USERS_WINDOWS_FINGERPRINT);23
const skip = enabled ? false : "Set CHJ_USERS_WINDOWS_* to run destructive tests on an isolated Windows test machine.";24
const HOST4 = env.CHJ_USERS_WINDOWS_HOST;25
const HOST6 = env.CHJ_USERS_WINDOWS_HOST6;26
const ADMIN = env.CHJ_USERS_WINDOWS_USER || "codex";27
const ADMIN_PASSWORD = env.CHJ_USERS_WINDOWS_PASSWORD;28
const FINGERPRINT_HEX = enabled ? Buffer.from(`${env.CHJ_USERS_WINDOWS_FINGERPRINT}=`, "base64").toString("hex") : "";29
const TEST_PASSWORD = `Chj-${crypto.randomBytes(6).toString("hex")}-9!`;30
const STANDARD_PASSWORD = `Chj-${crypto.randomBytes(6).toString("hex")}-7!`;31
const USER = "chjtst1";32
const STANDARD = "chjtst2";34
function login(host, username, { password, privateKey, keyboard = false } = {}) {35
return new Promise((resolve) => {36
const client = new Client();37
let done = false;38
const finish = (value) => { if (done) return; done = true; try { client.end(); } catch {} resolve(value); };39
client.on("ready", () => client.exec("echo ok", (error, stream) => {40
if (error) return finish(false);41
let out = ""; stream.on("data", (data) => { out += data; }); stream.stderr.resume();42
stream.on("close", () => finish(out.includes("ok")));43
}));44
client.on("error", () => finish(false));45
if (keyboard) client.on("keyboard-interactive", (_name, _instructions, _lang, prompts, reply) => reply(prompts.map(() => password)));46
client.connect({ host, port: 22, username, readyTimeout: 20000, hostHash: "sha256", hostVerifier: (hex) => hex === FINGERPRINT_HEX,47
...(privateKey ? { privateKey } : keyboard ? { tryKeyboard: true } : { password }) });48
});49
}51
class Profiles {52
constructor() { this.profiles = new Map(); this.passwords = new Map(); }53
add(profile, password) { this.profiles.set(profile.id, profile); if (password) this.passwords.set(profile.id, password); }54
get(id) { const profile = this.profiles.get(id); if (!profile) throw new Error("Profile not found"); return profile; }55
getHostKey() { return FINGERPRINT_HEX; }56
trustHostKey() {}57
getStoredPassword(id) { return this.passwords.get(id) || ""; }58
markUsed() {}59
}61
let manager; let api; let profiles; let executor; let originalConfig;62
// Windows may keep a profile hive loaded after SSH logons; then the account is deleted and its profile kept.63
async function deleteAccount(sid, username, probeCredentials) {64
try { return await api.manage("admin", { action: "delete", sid, username, removeProfile: true, probeCredentials }); }65
catch (error) {66
assert.equal(error.code, "USERS_PROFILE_IN_USE", error.message);67
const result = await api.manage("admin", { action: "delete", sid, username, removeProfile: false, probeCredentials });68
assert.ok(result.steps.some((step) => step.step === "profile" && step.status === "kept"));69
return result;70
}71
}72
async function admin(script, data = "") {73
// Test-only helper: runs a PowerShell snippet as the elevated test admin. Bulk data goes through stdin as $data,74
// because cmd.exe limits command lines to 8191 characters.75
const command = encodedCommand(`$ProgressPreference='SilentlyContinue'; $data=[Console]::In.ReadLine(); ${script}`);76
if (command.length > 8000) throw new Error("test helper command too long");77
const result = await manager._execStream(manager.sessions.get("admin"), command, { timeoutMs: 120000, onUpdate: (channel, kind) => { if (kind === "open") { channel.write(`${data}\n`); channel.end(); } } }).done;78
return result.stdout.trim();79
}80
const CONFIG_B64 = "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\"$env:ProgramData\\ssh\\sshd_config\"))";81
const RESTORE_CONFIG = "$p=\"$env:ProgramData\\ssh\\sshd_config\"; $t=\"$p.chj-test\"; [IO.File]::WriteAllBytes($t, [Convert]::FromBase64String($data)); Set-Acl $t (Get-Acl $p); [IO.File]::Replace($t, $p, [NullString]::Value)";83
test("chj.users on an isolated Windows machine through Windows OpenSSH", { skip, timeout: 1800000 }, async (t) => {84
profiles = new Profiles();85
profiles.add({ id: "admin", label: "win", host: HOST4, port: 22, username: ADMIN, authMethod: "password" }, ADMIN_PASSWORD);86
manager = new SessionManager({ profileService: profiles, latencyMonitor: { start: () => () => {} } });87
executor = new WindowsExecutor({ sessionManager: manager });88
const windowsService = new WindowsUserAdminService({ sessionManager: manager, windowsExecutor: executor });89
api = createUserAdminApi(new UserAdminService({ sessionManager: manager, privilegedExecutor: new PrivilegedExecutor({ sessionManager: manager }), windowsService, detectPlatform }));90
await manager.connect({ sessionId: "admin", profileId: "admin" });91
originalConfig = await admin(CONFIG_B64);92
assert.ok(originalConfig.length > 100, "sshd_config backup captured");93
const originalSddl = await admin("(Get-Acl \"$env:ProgramData\\ssh\\sshd_config\").Sddl");94
// DefaultShell is restored exactly (Windows Server may ship with PowerShell as the OpenSSH default shell).95
const originalShell = await admin("[string](Get-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -ErrorAction SilentlyContinue).DefaultShell");96
const restoreShell = originalShell97
? `New-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -Name DefaultShell -Value '${originalShell.replace(/'/g, "''")}' -PropertyType String -Force | Out-Null`98
: "Remove-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -Name DefaultShell -ErrorAction SilentlyContinue";99
t.after(async () => {100
try {101
if (!manager.sessions.get("admin")) await manager.connect({ sessionId: "admin", profileId: "admin" });102
await admin(`${restoreShell}103
if (${CONFIG_B64} -ne $data) { ${RESTORE_CONFIG}; Restart-Service sshd -Force }104
Start-Sleep -Seconds 5105
foreach ($name in @('${USER}','${STANDARD}','chjtst3')) { $u = Get-LocalUser -Name $name -ErrorAction SilentlyContinue; if ($u) { Remove-LocalUser -SID $u.SID; Get-CimInstance Win32_UserProfile -Filter "SID='$($u.SID.Value)'" | Remove-CimInstance -ErrorAction SilentlyContinue } }`, originalConfig);106
} finally { await manager.disconnectAll("test"); }107
});109
await t.test("platform detection uses Win32_OperatingSystem and the OpenSSH DefaultShell", async () => {110
const platform = await detectPlatform(manager, "admin");111
t.diagnostic(`${platform.caption} ${platform.version} ${platform.installationType} shell=${platform.defaultShell} ps=${platform.powershell}`);112
assert.equal(platform.os, "windows");113
assert.equal(platform.is64BitProcess, true);114
assert.equal(platform.localAccounts, true);115
});117
await t.test("privileges distinguish an elevated administrator token; no sudo is used", async () => {118
const privilege = await api.privileges("admin");119
assert.equal(privilege.platform, "windows");120
assert.equal(privilege.mode, "windows-elevated");121
assert.match(privilege.sid, /^S-1-5-21-/);122
assert.equal(privilege.sudoInstalled, false);123
});125
await t.test("inventory lists local accounts by SID, Administrators by S-1-5-32-544 and protects built-in accounts", async () => {126
const inventory = await api.inventory("admin");127
assert.equal(inventory.platform, "windows");128
const byRid = new Map(inventory.users.map((user) => [user.rid, user]));129
assert.equal(byRid.get(500).protection, "builtin");130
const self = inventory.users.find((user) => user.username.toLowerCase() === ADMIN.toLowerCase());131
assert.equal(self.protection, "session");132
assert.equal(self.admin, true);133
assert.ok(inventory.groups.some((group) => group.sid === "S-1-5-32-544"));134
assert.ok(inventory.users.length >= 3);135
assert.equal(JSON.stringify(inventory).includes("uid\":"), false, "no Linux UID is invented");136
});138
let testSid;139
await t.test("account lifecycle: create, password, names, disable/enable, Administrators membership", async () => {140
// Windows Server 2025 ships "AllowGroups administrators "openssh users""; the test account joins OpenSSH Users there.141
const sshGroups = /^\s*AllowGroups\b/im.test(Buffer.from(originalConfig, "base64").toString("utf8")) ? ["S-1-5-32-585"] : [];142
const created = await api.manage("admin", { action: "create", username: USER, password: TEST_PASSWORD, fullName: "CH-J Test", description: "CH-J integration test", groups: sshGroups });143
assert.equal(created.result, "ok", JSON.stringify(created.steps));144
const user = created.inventory.users.find((candidate) => candidate.username === USER);145
testSid = user.sid;146
assert.equal(user.enabled, true);147
assert.ok(user.groupSids.includes("S-1-5-32-545"), "member of Users");148
assert.equal(user.admin, false);149
assert.ok(await login(HOST4, USER, { password: TEST_PASSWORD }), "the new account can log in over SSH");150
await api.manage("admin", { action: "setFullName", sid: testSid, fullName: "Účet Test" });151
await api.manage("admin", { action: "setDescription", sid: testSid, description: "popis" });152
let result = await api.manage("admin", { action: "disable", sid: testSid });153
assert.equal(result.inventory.users.find((candidate) => candidate.sid === testSid).enabled, false);154
assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD }), false, "a disabled account cannot log in");155
await api.manage("admin", { action: "enable", sid: testSid });156
const newPassword = `${TEST_PASSWORD}x`;157
await api.manage("admin", { action: "setPassword", sid: testSid, password: newPassword });158
assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD }), false);159
assert.ok(await login(HOST4, USER, { password: newPassword }));160
await api.manage("admin", { action: "setPassword", sid: testSid, password: TEST_PASSWORD });161
result = await api.manage("admin", { action: "grantAdmin", sid: testSid });162
assert.equal(result.inventory.users.find((candidate) => candidate.sid === testSid).admin, true);163
result = await api.manage("admin", { action: "revokeAdmin", sid: testSid });164
assert.equal(result.inventory.users.find((candidate) => candidate.sid === testSid).admin, false);165
const detail = await api.detail("admin", { sid: testSid });166
assert.equal(detail.account.fullName, "Účet Test", "UTF-8 survives the PowerShell round trip");167
assert.equal(detail.account.description, "popis");168
assert.match(detail.account.profilePath || "", /chjtst1/i, "the profile exists after the first login");169
});171
await t.test("protected accounts, unsupported Linux actions and injection attempts are refused", async () => {172
const inventory = await api.inventory("admin");173
const builtin = inventory.users.find((user) => user.rid === 500);174
const self = inventory.users.find((user) => user.protection === "session");175
await assert.rejects(() => api.manage("admin", { action: "disable", sid: builtin.sid }), { code: "USERS_PROTECTED_USER" });176
await assert.rejects(() => api.manage("admin", { action: "delete", sid: self.sid }), { code: "USERS_PROTECTED_USER" });177
await assert.rejects(() => api.manage("admin", { action: "setShell", sid: testSid, shell: "/bin/sh" }), { code: "USERS_NOT_SUPPORTED_ON_PLATFORM" });178
for (const username of ["a;b", "x$(whoami)", "evil`n", "a\"b", "bad/name", "toolongname_abcdefghij", "dot."]) {179
await assert.rejects(() => api.manage("admin", { action: "create", username, password: TEST_PASSWORD }), { code: "USERS_NAME_INVALID" }, username);180
}181
await assert.rejects(() => api.manage("admin", { action: "disable", sid: "S-1-5-21-1;whoami" }), { code: "USERS_SID_INVALID" });182
await assert.rejects(() => api.manage("admin", { action: "addGroup", sid: testSid, group: "Administrators" }), { code: "USERS_GROUP_INVALID" });183
// The script validates again when Core is bypassed.184
await assert.rejects(() => executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "account", action: "disable", sid: "S-1-5-21-1-2-3-500'; Remove-LocalUser x; '" } }), { code: "USERS_SID_INVALID" });185
await assert.rejects(() => executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "keys-write", sid: testSid, path: "C:\\Windows\\win.ini", expected: "absent", data: "" } }), { code: "USERS_KEYS_PATH_UNSUPPORTED" });186
});188
const generator = new KeyGeneratorService();189
const keyA = generator.generate({ type: "ed25519", comment: "chj-a", privateKeyFormat: "openssh" });190
const keyB = generator.generate({ type: "ed25519", comment: "chj-b", privateKeyFormat: "openssh" });191
await t.test("user authorized_keys: add/remove one key, keep other lines and a strict ACL", async () => {192
const unrelated = "# foreign\r\nfrom=\"10.0.0.0/8\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl legacy\r\n";193
await admin(`$d=(Get-ItemProperty "HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\${testSid}").ProfileImagePath + '\\.ssh'; New-Item -ItemType Directory -Force $d | Out-Null; [IO.File]::WriteAllBytes("$d\\authorized_keys", [Convert]::FromBase64String($data))194
icacls "$d\\authorized_keys" /inheritance:r /grant:r "*${testSid}:F" "*S-1-5-18:F" "*S-1-5-32-544:F" | Out-Null`, Buffer.from(unrelated).toString("base64"));195
let result = await api.keys("admin", { action: "add", sid: testSid, publicKey: keyA.publicKey });196
result = await api.keys("admin", { action: "add", sid: testSid, publicKey: keyB.publicKey });197
await assert.rejects(() => api.keys("admin", { action: "add", sid: testSid, publicKey: keyB.publicKey }), { code: "USERS_KEY_DUPLICATE" });198
const file = result.detail.keys.files[0];199
assert.equal(file.shared, false);200
assert.match(file.path, /\\Users\\chjtst1[^\\]*\\\.ssh\\authorized_keys$/i);201
assert.equal(file.entries.filter((entry) => entry.valid).length, 3);202
assert.ok(await login(HOST4, USER, { privateKey: keyA.privateKey }), "key A works over IPv4");203
if (HOST6) assert.ok(await login(HOST6, USER, { privateKey: keyB.privateKey }), "key B works over IPv6");204
const entryA = file.entries.find((entry) => entry.comment === "chj-a");205
result = await api.keys("admin", { action: "remove", sid: testSid, path: file.path, line: entryA.line, fingerprint: entryA.fingerprint });206
assert.equal(await login(HOST4, USER, { privateKey: keyA.privateKey }), false);207
assert.ok(await login(HOST4, USER, { privateKey: keyB.privateKey }));208
const bytes = Buffer.from(await admin(`[Convert]::ToBase64String([IO.File]::ReadAllBytes('${file.path}'))`), "base64").toString("latin1");209
assert.equal(bytes, `${unrelated}${keyB.publicKey}\n`, "foreign lines and CRLF are byte-identical");210
const sids = (await admin(`(Get-Acl '${file.path}').Access | % { $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value } | Sort-Object`)).split(/\r?\n/);211
assert.deepEqual(sids.sort(), [testSid, "S-1-5-18", "S-1-5-32-544"].sort(), "the existing ACL is kept");212
});214
await t.test("administrators_authorized_keys is shared, protected and reported as such", async () => {215
await api.manage("admin", { action: "grantAdmin", sid: testSid });216
const before = await admin("Test-Path \"$env:ProgramData\\ssh\\administrators_authorized_keys\"");217
const result = await api.keys("admin", { action: "add", sid: testSid, publicKey: keyA.publicKey });218
const file = result.detail.keys.files[0];219
assert.equal(file.shared, true);220
assert.match(file.path, /ProgramData\\ssh\\administrators_authorized_keys$/i);221
assert.ok(result.detail.ssh.warnings.some((warning) => /share one administrators_authorized_keys/.test(warning)));222
assert.ok(await login(HOST4, USER, { privateKey: keyA.privateKey }), "the admin key works");223
const sids = (await admin("(Get-Acl \"$env:ProgramData\\ssh\\administrators_authorized_keys\").Access | % { $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value } | Sort-Object")).split(/\r?\n/);224
assert.deepEqual(sids, ["S-1-5-18", "S-1-5-32-544"]);225
const entry = file.entries.find((candidate) => candidate.comment === "chj-a");226
await api.keys("admin", { action: "remove", sid: testSid, path: file.path, line: entry.line, fingerprint: entry.fingerprint });227
if (before === "False") await admin("$f=\"$env:ProgramData\\ssh\\administrators_authorized_keys\"; if ((Get-Item $f).Length -eq 0) { Remove-Item $f }");228
await api.manage("admin", { action: "revokeAdmin", sid: testSid });229
});231
const probeCredentials = { password: ADMIN_PASSWORD };232
await t.test("per-user SSH policy: password, IPv4 and IPv6 enforced separately and verified by real logins", async () => {233
const original = Buffer.from(originalConfig, "base64").toString("utf8");234
let result = await api.sshPolicy("admin", { sid: testSid, policy: { password: "no" }, probeCredentials });235
assert.equal(result.applied, true); assert.equal(result.restarted, true);236
const config = Buffer.from(await admin(CONFIG_B64), "base64").toString("utf8");237
assert.doesNotMatch(config, /KbdInteractiveAuthentication/i, "no unsupported directive is written");238
assert.ok(config.indexOf("# BEGIN CH-J chj.users global") < config.search(/^Match Group administrators/m), "global section stays before the existing Match block");239
assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD }), false, "password login disabled");240
assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD, keyboard: true }), false, "keyboard-interactive does not reopen password login");241
assert.ok(await login(HOST4, USER, { privateKey: keyB.privateKey }), "key login still works");242
assert.ok(await login(HOST4, ADMIN, { password: ADMIN_PASSWORD }), "other accounts keep password login");243
if (HOST6) {244
result = await api.sshPolicy("admin", { sid: testSid, policy: { ipv4: "deny" }, probeCredentials });245
assert.equal(result.detail.ssh.effective.ipv4.allowed, false);246
assert.equal(await login(HOST4, USER, { privateKey: keyB.privateKey }), false, "IPv4 denied");247
assert.ok(await login(HOST6, USER, { privateKey: keyB.privateKey }), "IPv6 allowed");248
result = await api.sshPolicy("admin", { sid: testSid, policy: { ipv6: "deny" }, probeCredentials });249
assert.ok(await login(HOST4, USER, { privateKey: keyB.privateKey }), "IPv4 allowed");250
assert.equal(await login(HOST6, USER, { privateKey: keyB.privateKey }), false, "IPv6 denied");251
}252
result = await api.sshPolicy("admin", { sid: testSid, policy: { access: "deny" }, probeCredentials });253
assert.equal(await login(HOST4, USER, { privateKey: keyB.privateKey }), false, "SSH fully disabled");254
result = await api.sshPolicy("admin", { sid: testSid, policy: {}, probeCredentials });255
const restored = Buffer.from(await admin(CONFIG_B64), "base64").toString("utf8");256
assert.equal(restored, original, "inherit everywhere restores the original sshd_config byte for byte");257
assert.equal(await admin("(Get-Acl \"$env:ProgramData\\ssh\\sshd_config\").Sddl"), originalSddl, "the sshd_config ACL is preserved");258
assert.ok(await login(HOST4, USER, { password: TEST_PASSWORD }));259
});261
await t.test("existing Match rules, missing test-login credentials and lockout of the session account", async () => {262
await admin(`$p="$env:ProgramData\\ssh\\sshd_config"; Add-Content -LiteralPath $p -Value "Match User ${USER}"," PasswordAuthentication yes" -Encoding ASCII`);263
await assert.rejects(() => api.sshPolicy("admin", { sid: testSid, policy: { password: "no" }, probeCredentials }), { code: "USERS_SSH_POLICY_NOT_EFFECTIVE" });264
await admin(RESTORE_CONFIG, originalConfig);265
profiles.passwords.delete("admin");266
await assert.rejects(() => api.sshPolicy("admin", { sid: testSid, policy: { password: "no" } }), { code: "USERS_SSH_PROBE_CREDENTIALS_REQUIRED" });267
profiles.passwords.set("admin", ADMIN_PASSWORD);268
const self = (await api.inventory("admin")).users.find((user) => user.protection === "session");269
await assert.rejects(() => api.sshPolicy("admin", { sid: self.sid, policy: { password: "no" }, probeCredentials }), { code: "USERS_SSH_LOCKOUT_PREVENTED" });270
await assert.rejects(() => api.sshPolicy("admin", { sid: self.sid, policy: { ipv4: "deny" }, probeCredentials }), { code: "USERS_SSH_LOCKOUT_PREVENTED" });271
assert.equal(await admin(CONFIG_B64), originalConfig, "nothing was written");272
});274
await t.test("a standard account works read-only and gets a precise elevation error", async () => {275
const sshGroups = /^\s*AllowGroups\b/im.test(Buffer.from(originalConfig, "base64").toString("utf8")) ? ["S-1-5-32-585"] : [];276
await api.manage("admin", { action: "create", username: STANDARD, password: STANDARD_PASSWORD, groups: sshGroups });277
profiles.add({ id: "standard", label: "win", host: HOST4, port: 22, username: STANDARD, authMethod: "password" }, STANDARD_PASSWORD);278
await manager.connect({ sessionId: "standard", profileId: "standard" });279
const privilege = await api.privileges("standard");280
assert.equal(privilege.mode, "windows-standard");281
const inventory = await api.inventory("standard");282
assert.equal(inventory.privileged, false);283
assert.ok(inventory.users.length >= 3, "the account list is not empty in read-only mode");284
await assert.rejects(() => api.manage("standard", { action: "disable", sid: testSid }), { code: "USERS_WINDOWS_ELEVATION_REQUIRED" });285
await assert.rejects(() => api.sshPolicy("standard", { sid: testSid, policy: { password: "no" } }), { code: "USERS_WINDOWS_ELEVATION_REQUIRED" });286
const own = await api.detail("standard", { username: STANDARD });287
assert.equal(own.self, true);288
const other = await api.detail("standard", { sid: testSid });289
assert.equal(other.keys.files[0].reason, "needs-elevation");290
await manager.disconnect("standard");291
const standardSid = (await api.inventory("admin")).users.find((user) => user.username === STANDARD).sid;292
await admin(`$end=(Get-Date).AddSeconds(60); while ((Get-CimInstance Win32_UserProfile -Filter "SID='${standardSid}'").Loaded -and (Get-Date) -lt $end) { Start-Sleep 2 }`);293
const removed = await deleteAccount(standardSid, STANDARD, {});294
assert.equal(removed.inventory.users.some((user) => user.sid === standardSid), false);295
});297
await t.test("Windows PowerShell as OpenSSH DefaultShell is detected and works", async () => {298
await admin("New-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -Name DefaultShell -Value \"$env:WINDIR\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -PropertyType String -Force | Out-Null");299
profiles.add({ id: "ps", label: "win", host: HOST4, port: 22, username: ADMIN, authMethod: "password" }, ADMIN_PASSWORD);300
await manager.connect({ sessionId: "ps", profileId: "ps" });301
const platform = await detectPlatform(manager, "ps");302
assert.equal(platform.defaultShell, "powershell");303
assert.ok((await api.inventory("ps")).users.length >= 3);304
await manager.disconnect("ps");305
await admin(restoreShell);306
});308
await t.test("deleting an account removes its profile and CH-J SSH rules", async () => {309
await api.sshPolicy("admin", { sid: testSid, policy: { ipv6: "deny" }, probeCredentials });310
await admin(`$end=(Get-Date).AddSeconds(60); while ((Get-CimInstance Win32_UserProfile -Filter "SID='${testSid}'").Loaded -and (Get-Date) -lt $end) { Start-Sleep 2 }`);311
const result = await deleteAccount(testSid, USER, probeCredentials);312
assert.notEqual(result.result, "failed", JSON.stringify(result));313
assert.equal(result.inventory.users.some((user) => user.sid === testSid), false);314
assert.equal(await admin(CONFIG_B64), originalConfig, "CH-J SSH rules of the deleted account were removed");315
});317
await t.test("rollback guard restores sshd_config when CH-J disappears or the test login fails", { skip: env.CHJ_USERS_WINDOWS_SLOW === "1" ? false : "Set CHJ_USERS_WINDOWS_SLOW=1 (takes about 5 minutes)." }, async () => {318
const { renderConfig } = require("../src/main/users/windowsSshPolicy");319
const original = Buffer.from(originalConfig, "base64");320
const content = renderConfig(original.toString("utf8"), new Map([["chjtst3", { access: "deny", password: "inherit", pubkey: "inherit", ipv4: "allow", ipv6: "allow" }]]));321
const sha = crypto.createHash("sha256").update(original).digest("hex");322
const staged = await executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "ssh-stage", expected: sha, data: Buffer.from(content).toString("base64"), tuples: [] } });323
const nonce = crypto.randomBytes(24).toString("hex");324
const commit = await executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "ssh-commit", id: staged.id, nonce, expected: sha, candidateSha256: staged.candidateSha256, timeoutSeconds: 60 } });325
assert.equal(commit.state, "awaiting-confirmation");326
assert.match(await admin("Get-Content \"$env:ProgramData\\ssh\\sshd_config\" -Raw"), /DenyUsers chjtst3/, "the change is live");327
await manager.disconnect("admin"); // simulates CH-J crashing before confirmation328
await new Promise((resolve) => setTimeout(resolve, 90000));329
await manager.connect({ sessionId: "admin", profileId: "admin" });330
assert.equal(await admin(CONFIG_B64), originalConfig, "restored without CH-J");331
const status = await executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "ssh-confirm", id: staged.id, nonce, confirm: false, waitSeconds: 1 } });332
assert.equal(status.state, "rolled-back");333
// A failing test login (wrong credentials) never confirms; the guard restores the previous file.334
await api.manage("admin", { action: "create", username: "chjtst3", password: TEST_PASSWORD });335
const sid = (await api.inventory("admin")).users.find((user) => user.username === "chjtst3").sid;336
await assert.rejects(() => api.sshPolicy("admin", { sid, policy: { password: "no" }, probeCredentials: { password: "wrong-password" } }), { code: "USERS_SSH_PROBE_FAILED" });337
if (!manager.sessions.get("admin")) await manager.connect({ sessionId: "admin", profileId: "admin" });338
assert.equal(await admin(CONFIG_B64), originalConfig);339
await api.manage("admin", { action: "delete", sid, username: "chjtst3", removeProfile: true });340
});341
});SHA-256
50cd036527497f9ec988a2f21d861b6849be4ff0d4763a692e1aaa990fb2d9f2CH-J Proprietary Software License 1.14 · Izvor je osiguran pod CH-J Proprietary Software License 1.14. Njena dostupnost ne mijenja uvjete licenciranja niti daje dodatna prava.
Arhiv SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a