"use strict"; // Destructive end-to-end tests of chj.users against an ISOLATED Windows test machine (never production). // Creates temporary local accounts "chjtst*", changes sshd_config through CH-J (restarting sshd) and restores // the original sshd_config at the end. Credentials come only from the environment: // CHJ_USERS_WINDOWS_HOST=192.0.2.5 CHJ_USERS_WINDOWS_HOST6=2001:db8::5 CHJ_USERS_WINDOWS_USER=admin // CHJ_USERS_WINDOWS_PASSWORD=... CHJ_USERS_WINDOWS_FINGERPRINT= node --test test/usersWindowsIntegration.test.js // Optional: CHJ_USERS_WINDOWS_SLOW=1 also runs the 2-minute rollback-guard scenarios. const test = require("node:test"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); const { Client } = require("ssh2"); const { SessionManager } = require("../src/main/sessions/sessionManager"); const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec"); const { WindowsExecutor, encodedCommand } = require("../src/main/sessions/windowsPrivilegedExec"); const { detectPlatform } = require("../src/main/sessions/platformDetect"); const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService"); const { WindowsUserAdminService, REMOTE_SCRIPT } = require("../src/main/users/windowsUserAdminService"); const { KeyGeneratorService } = require("../src/main/keys/keyGeneratorService"); const env = process.env; const enabled = Boolean(env.CHJ_USERS_WINDOWS_HOST && env.CHJ_USERS_WINDOWS_PASSWORD && env.CHJ_USERS_WINDOWS_FINGERPRINT); const skip = enabled ? false : "Set CHJ_USERS_WINDOWS_* to run destructive tests on an isolated Windows test machine."; const HOST4 = env.CHJ_USERS_WINDOWS_HOST; const HOST6 = env.CHJ_USERS_WINDOWS_HOST6; const ADMIN = env.CHJ_USERS_WINDOWS_USER || "codex"; const ADMIN_PASSWORD = env.CHJ_USERS_WINDOWS_PASSWORD; const FINGERPRINT_HEX = enabled ? Buffer.from(`${env.CHJ_USERS_WINDOWS_FINGERPRINT}=`, "base64").toString("hex") : ""; const TEST_PASSWORD = `Chj-${crypto.randomBytes(6).toString("hex")}-9!`; const STANDARD_PASSWORD = `Chj-${crypto.randomBytes(6).toString("hex")}-7!`; const USER = "chjtst1"; const STANDARD = "chjtst2"; function login(host, username, { password, privateKey, keyboard = false } = {}) { return new Promise((resolve) => { const client = new Client(); let done = false; const finish = (value) => { if (done) return; done = true; try { client.end(); } catch {} resolve(value); }; client.on("ready", () => client.exec("echo ok", (error, stream) => { if (error) return finish(false); let out = ""; stream.on("data", (data) => { out += data; }); stream.stderr.resume(); stream.on("close", () => finish(out.includes("ok"))); })); client.on("error", () => finish(false)); if (keyboard) client.on("keyboard-interactive", (_name, _instructions, _lang, prompts, reply) => reply(prompts.map(() => password))); client.connect({ host, port: 22, username, readyTimeout: 20000, hostHash: "sha256", hostVerifier: (hex) => hex === FINGERPRINT_HEX, ...(privateKey ? { privateKey } : keyboard ? { tryKeyboard: true } : { password }) }); }); } class Profiles { constructor() { this.profiles = new Map(); this.passwords = new Map(); } add(profile, password) { this.profiles.set(profile.id, profile); if (password) this.passwords.set(profile.id, password); } get(id) { const profile = this.profiles.get(id); if (!profile) throw new Error("Profile not found"); return profile; } getHostKey() { return FINGERPRINT_HEX; } trustHostKey() {} getStoredPassword(id) { return this.passwords.get(id) || ""; } markUsed() {} } let manager; let api; let profiles; let executor; let originalConfig; // Windows may keep a profile hive loaded after SSH logons; then the account is deleted and its profile kept. async function deleteAccount(sid, username, probeCredentials) { try { return await api.manage("admin", { action: "delete", sid, username, removeProfile: true, probeCredentials }); } catch (error) { assert.equal(error.code, "USERS_PROFILE_IN_USE", error.message); const result = await api.manage("admin", { action: "delete", sid, username, removeProfile: false, probeCredentials }); assert.ok(result.steps.some((step) => step.step === "profile" && step.status === "kept")); return result; } } async function admin(script, data = "") { // Test-only helper: runs a PowerShell snippet as the elevated test admin. Bulk data goes through stdin as $data, // because cmd.exe limits command lines to 8191 characters. const command = encodedCommand(`$ProgressPreference='SilentlyContinue'; $data=[Console]::In.ReadLine(); ${script}`); if (command.length > 8000) throw new Error("test helper command too long"); 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; return result.stdout.trim(); } const CONFIG_B64 = "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\"$env:ProgramData\\ssh\\sshd_config\"))"; 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)"; test("chj.users on an isolated Windows machine through Windows OpenSSH", { skip, timeout: 1800000 }, async (t) => { profiles = new Profiles(); profiles.add({ id: "admin", label: "win", host: HOST4, port: 22, username: ADMIN, authMethod: "password" }, ADMIN_PASSWORD); manager = new SessionManager({ profileService: profiles, latencyMonitor: { start: () => () => {} } }); executor = new WindowsExecutor({ sessionManager: manager }); const windowsService = new WindowsUserAdminService({ sessionManager: manager, windowsExecutor: executor }); api = createUserAdminApi(new UserAdminService({ sessionManager: manager, privilegedExecutor: new PrivilegedExecutor({ sessionManager: manager }), windowsService, detectPlatform })); await manager.connect({ sessionId: "admin", profileId: "admin" }); originalConfig = await admin(CONFIG_B64); assert.ok(originalConfig.length > 100, "sshd_config backup captured"); const originalSddl = await admin("(Get-Acl \"$env:ProgramData\\ssh\\sshd_config\").Sddl"); // DefaultShell is restored exactly (Windows Server may ship with PowerShell as the OpenSSH default shell). const originalShell = await admin("[string](Get-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -ErrorAction SilentlyContinue).DefaultShell"); const restoreShell = originalShell ? `New-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -Name DefaultShell -Value '${originalShell.replace(/'/g, "''")}' -PropertyType String -Force | Out-Null` : "Remove-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -Name DefaultShell -ErrorAction SilentlyContinue"; t.after(async () => { try { if (!manager.sessions.get("admin")) await manager.connect({ sessionId: "admin", profileId: "admin" }); await admin(`${restoreShell} if (${CONFIG_B64} -ne $data) { ${RESTORE_CONFIG}; Restart-Service sshd -Force } Start-Sleep -Seconds 5 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); } finally { await manager.disconnectAll("test"); } }); await t.test("platform detection uses Win32_OperatingSystem and the OpenSSH DefaultShell", async () => { const platform = await detectPlatform(manager, "admin"); t.diagnostic(`${platform.caption} ${platform.version} ${platform.installationType} shell=${platform.defaultShell} ps=${platform.powershell}`); assert.equal(platform.os, "windows"); assert.equal(platform.is64BitProcess, true); assert.equal(platform.localAccounts, true); }); await t.test("privileges distinguish an elevated administrator token; no sudo is used", async () => { const privilege = await api.privileges("admin"); assert.equal(privilege.platform, "windows"); assert.equal(privilege.mode, "windows-elevated"); assert.match(privilege.sid, /^S-1-5-21-/); assert.equal(privilege.sudoInstalled, false); }); await t.test("inventory lists local accounts by SID, Administrators by S-1-5-32-544 and protects built-in accounts", async () => { const inventory = await api.inventory("admin"); assert.equal(inventory.platform, "windows"); const byRid = new Map(inventory.users.map((user) => [user.rid, user])); assert.equal(byRid.get(500).protection, "builtin"); const self = inventory.users.find((user) => user.username.toLowerCase() === ADMIN.toLowerCase()); assert.equal(self.protection, "session"); assert.equal(self.admin, true); assert.ok(inventory.groups.some((group) => group.sid === "S-1-5-32-544")); assert.ok(inventory.users.length >= 3); assert.equal(JSON.stringify(inventory).includes("uid\":"), false, "no Linux UID is invented"); }); let testSid; await t.test("account lifecycle: create, password, names, disable/enable, Administrators membership", async () => { // Windows Server 2025 ships "AllowGroups administrators "openssh users""; the test account joins OpenSSH Users there. const sshGroups = /^\s*AllowGroups\b/im.test(Buffer.from(originalConfig, "base64").toString("utf8")) ? ["S-1-5-32-585"] : []; const created = await api.manage("admin", { action: "create", username: USER, password: TEST_PASSWORD, fullName: "CH-J Test", description: "CH-J integration test", groups: sshGroups }); assert.equal(created.result, "ok", JSON.stringify(created.steps)); const user = created.inventory.users.find((candidate) => candidate.username === USER); testSid = user.sid; assert.equal(user.enabled, true); assert.ok(user.groupSids.includes("S-1-5-32-545"), "member of Users"); assert.equal(user.admin, false); assert.ok(await login(HOST4, USER, { password: TEST_PASSWORD }), "the new account can log in over SSH"); await api.manage("admin", { action: "setFullName", sid: testSid, fullName: "Účet Test" }); await api.manage("admin", { action: "setDescription", sid: testSid, description: "popis" }); let result = await api.manage("admin", { action: "disable", sid: testSid }); assert.equal(result.inventory.users.find((candidate) => candidate.sid === testSid).enabled, false); assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD }), false, "a disabled account cannot log in"); await api.manage("admin", { action: "enable", sid: testSid }); const newPassword = `${TEST_PASSWORD}x`; await api.manage("admin", { action: "setPassword", sid: testSid, password: newPassword }); assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD }), false); assert.ok(await login(HOST4, USER, { password: newPassword })); await api.manage("admin", { action: "setPassword", sid: testSid, password: TEST_PASSWORD }); result = await api.manage("admin", { action: "grantAdmin", sid: testSid }); assert.equal(result.inventory.users.find((candidate) => candidate.sid === testSid).admin, true); result = await api.manage("admin", { action: "revokeAdmin", sid: testSid }); assert.equal(result.inventory.users.find((candidate) => candidate.sid === testSid).admin, false); const detail = await api.detail("admin", { sid: testSid }); assert.equal(detail.account.fullName, "Účet Test", "UTF-8 survives the PowerShell round trip"); assert.equal(detail.account.description, "popis"); assert.match(detail.account.profilePath || "", /chjtst1/i, "the profile exists after the first login"); }); await t.test("protected accounts, unsupported Linux actions and injection attempts are refused", async () => { const inventory = await api.inventory("admin"); const builtin = inventory.users.find((user) => user.rid === 500); const self = inventory.users.find((user) => user.protection === "session"); await assert.rejects(() => api.manage("admin", { action: "disable", sid: builtin.sid }), { code: "USERS_PROTECTED_USER" }); await assert.rejects(() => api.manage("admin", { action: "delete", sid: self.sid }), { code: "USERS_PROTECTED_USER" }); await assert.rejects(() => api.manage("admin", { action: "setShell", sid: testSid, shell: "/bin/sh" }), { code: "USERS_NOT_SUPPORTED_ON_PLATFORM" }); for (const username of ["a;b", "x$(whoami)", "evil`n", "a\"b", "bad/name", "toolongname_abcdefghij", "dot."]) { await assert.rejects(() => api.manage("admin", { action: "create", username, password: TEST_PASSWORD }), { code: "USERS_NAME_INVALID" }, username); } await assert.rejects(() => api.manage("admin", { action: "disable", sid: "S-1-5-21-1;whoami" }), { code: "USERS_SID_INVALID" }); await assert.rejects(() => api.manage("admin", { action: "addGroup", sid: testSid, group: "Administrators" }), { code: "USERS_GROUP_INVALID" }); // The script validates again when Core is bypassed. 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" }); 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" }); }); const generator = new KeyGeneratorService(); const keyA = generator.generate({ type: "ed25519", comment: "chj-a", privateKeyFormat: "openssh" }); const keyB = generator.generate({ type: "ed25519", comment: "chj-b", privateKeyFormat: "openssh" }); await t.test("user authorized_keys: add/remove one key, keep other lines and a strict ACL", async () => { const unrelated = "# foreign\r\nfrom=\"10.0.0.0/8\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl legacy\r\n"; 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)) 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")); let result = await api.keys("admin", { action: "add", sid: testSid, publicKey: keyA.publicKey }); result = await api.keys("admin", { action: "add", sid: testSid, publicKey: keyB.publicKey }); await assert.rejects(() => api.keys("admin", { action: "add", sid: testSid, publicKey: keyB.publicKey }), { code: "USERS_KEY_DUPLICATE" }); const file = result.detail.keys.files[0]; assert.equal(file.shared, false); assert.match(file.path, /\\Users\\chjtst1[^\\]*\\\.ssh\\authorized_keys$/i); assert.equal(file.entries.filter((entry) => entry.valid).length, 3); assert.ok(await login(HOST4, USER, { privateKey: keyA.privateKey }), "key A works over IPv4"); if (HOST6) assert.ok(await login(HOST6, USER, { privateKey: keyB.privateKey }), "key B works over IPv6"); const entryA = file.entries.find((entry) => entry.comment === "chj-a"); result = await api.keys("admin", { action: "remove", sid: testSid, path: file.path, line: entryA.line, fingerprint: entryA.fingerprint }); assert.equal(await login(HOST4, USER, { privateKey: keyA.privateKey }), false); assert.ok(await login(HOST4, USER, { privateKey: keyB.privateKey })); const bytes = Buffer.from(await admin(`[Convert]::ToBase64String([IO.File]::ReadAllBytes('${file.path}'))`), "base64").toString("latin1"); assert.equal(bytes, `${unrelated}${keyB.publicKey}\n`, "foreign lines and CRLF are byte-identical"); const sids = (await admin(`(Get-Acl '${file.path}').Access | % { $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value } | Sort-Object`)).split(/\r?\n/); assert.deepEqual(sids.sort(), [testSid, "S-1-5-18", "S-1-5-32-544"].sort(), "the existing ACL is kept"); }); await t.test("administrators_authorized_keys is shared, protected and reported as such", async () => { await api.manage("admin", { action: "grantAdmin", sid: testSid }); const before = await admin("Test-Path \"$env:ProgramData\\ssh\\administrators_authorized_keys\""); const result = await api.keys("admin", { action: "add", sid: testSid, publicKey: keyA.publicKey }); const file = result.detail.keys.files[0]; assert.equal(file.shared, true); assert.match(file.path, /ProgramData\\ssh\\administrators_authorized_keys$/i); assert.ok(result.detail.ssh.warnings.some((warning) => /share one administrators_authorized_keys/.test(warning))); assert.ok(await login(HOST4, USER, { privateKey: keyA.privateKey }), "the admin key works"); const sids = (await admin("(Get-Acl \"$env:ProgramData\\ssh\\administrators_authorized_keys\").Access | % { $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value } | Sort-Object")).split(/\r?\n/); assert.deepEqual(sids, ["S-1-5-18", "S-1-5-32-544"]); const entry = file.entries.find((candidate) => candidate.comment === "chj-a"); await api.keys("admin", { action: "remove", sid: testSid, path: file.path, line: entry.line, fingerprint: entry.fingerprint }); if (before === "False") await admin("$f=\"$env:ProgramData\\ssh\\administrators_authorized_keys\"; if ((Get-Item $f).Length -eq 0) { Remove-Item $f }"); await api.manage("admin", { action: "revokeAdmin", sid: testSid }); }); const probeCredentials = { password: ADMIN_PASSWORD }; await t.test("per-user SSH policy: password, IPv4 and IPv6 enforced separately and verified by real logins", async () => { const original = Buffer.from(originalConfig, "base64").toString("utf8"); let result = await api.sshPolicy("admin", { sid: testSid, policy: { password: "no" }, probeCredentials }); assert.equal(result.applied, true); assert.equal(result.restarted, true); const config = Buffer.from(await admin(CONFIG_B64), "base64").toString("utf8"); assert.doesNotMatch(config, /KbdInteractiveAuthentication/i, "no unsupported directive is written"); assert.ok(config.indexOf("# BEGIN CH-J chj.users global") < config.search(/^Match Group administrators/m), "global section stays before the existing Match block"); assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD }), false, "password login disabled"); assert.equal(await login(HOST4, USER, { password: TEST_PASSWORD, keyboard: true }), false, "keyboard-interactive does not reopen password login"); assert.ok(await login(HOST4, USER, { privateKey: keyB.privateKey }), "key login still works"); assert.ok(await login(HOST4, ADMIN, { password: ADMIN_PASSWORD }), "other accounts keep password login"); if (HOST6) { result = await api.sshPolicy("admin", { sid: testSid, policy: { ipv4: "deny" }, probeCredentials }); assert.equal(result.detail.ssh.effective.ipv4.allowed, false); assert.equal(await login(HOST4, USER, { privateKey: keyB.privateKey }), false, "IPv4 denied"); assert.ok(await login(HOST6, USER, { privateKey: keyB.privateKey }), "IPv6 allowed"); result = await api.sshPolicy("admin", { sid: testSid, policy: { ipv6: "deny" }, probeCredentials }); assert.ok(await login(HOST4, USER, { privateKey: keyB.privateKey }), "IPv4 allowed"); assert.equal(await login(HOST6, USER, { privateKey: keyB.privateKey }), false, "IPv6 denied"); } result = await api.sshPolicy("admin", { sid: testSid, policy: { access: "deny" }, probeCredentials }); assert.equal(await login(HOST4, USER, { privateKey: keyB.privateKey }), false, "SSH fully disabled"); result = await api.sshPolicy("admin", { sid: testSid, policy: {}, probeCredentials }); const restored = Buffer.from(await admin(CONFIG_B64), "base64").toString("utf8"); assert.equal(restored, original, "inherit everywhere restores the original sshd_config byte for byte"); assert.equal(await admin("(Get-Acl \"$env:ProgramData\\ssh\\sshd_config\").Sddl"), originalSddl, "the sshd_config ACL is preserved"); assert.ok(await login(HOST4, USER, { password: TEST_PASSWORD })); }); await t.test("existing Match rules, missing test-login credentials and lockout of the session account", async () => { await admin(`$p="$env:ProgramData\\ssh\\sshd_config"; Add-Content -LiteralPath $p -Value "Match User ${USER}"," PasswordAuthentication yes" -Encoding ASCII`); await assert.rejects(() => api.sshPolicy("admin", { sid: testSid, policy: { password: "no" }, probeCredentials }), { code: "USERS_SSH_POLICY_NOT_EFFECTIVE" }); await admin(RESTORE_CONFIG, originalConfig); profiles.passwords.delete("admin"); await assert.rejects(() => api.sshPolicy("admin", { sid: testSid, policy: { password: "no" } }), { code: "USERS_SSH_PROBE_CREDENTIALS_REQUIRED" }); profiles.passwords.set("admin", ADMIN_PASSWORD); const self = (await api.inventory("admin")).users.find((user) => user.protection === "session"); await assert.rejects(() => api.sshPolicy("admin", { sid: self.sid, policy: { password: "no" }, probeCredentials }), { code: "USERS_SSH_LOCKOUT_PREVENTED" }); await assert.rejects(() => api.sshPolicy("admin", { sid: self.sid, policy: { ipv4: "deny" }, probeCredentials }), { code: "USERS_SSH_LOCKOUT_PREVENTED" }); assert.equal(await admin(CONFIG_B64), originalConfig, "nothing was written"); }); await t.test("a standard account works read-only and gets a precise elevation error", async () => { const sshGroups = /^\s*AllowGroups\b/im.test(Buffer.from(originalConfig, "base64").toString("utf8")) ? ["S-1-5-32-585"] : []; await api.manage("admin", { action: "create", username: STANDARD, password: STANDARD_PASSWORD, groups: sshGroups }); profiles.add({ id: "standard", label: "win", host: HOST4, port: 22, username: STANDARD, authMethod: "password" }, STANDARD_PASSWORD); await manager.connect({ sessionId: "standard", profileId: "standard" }); const privilege = await api.privileges("standard"); assert.equal(privilege.mode, "windows-standard"); const inventory = await api.inventory("standard"); assert.equal(inventory.privileged, false); assert.ok(inventory.users.length >= 3, "the account list is not empty in read-only mode"); await assert.rejects(() => api.manage("standard", { action: "disable", sid: testSid }), { code: "USERS_WINDOWS_ELEVATION_REQUIRED" }); await assert.rejects(() => api.sshPolicy("standard", { sid: testSid, policy: { password: "no" } }), { code: "USERS_WINDOWS_ELEVATION_REQUIRED" }); const own = await api.detail("standard", { username: STANDARD }); assert.equal(own.self, true); const other = await api.detail("standard", { sid: testSid }); assert.equal(other.keys.files[0].reason, "needs-elevation"); await manager.disconnect("standard"); const standardSid = (await api.inventory("admin")).users.find((user) => user.username === STANDARD).sid; await admin(`$end=(Get-Date).AddSeconds(60); while ((Get-CimInstance Win32_UserProfile -Filter "SID='${standardSid}'").Loaded -and (Get-Date) -lt $end) { Start-Sleep 2 }`); const removed = await deleteAccount(standardSid, STANDARD, {}); assert.equal(removed.inventory.users.some((user) => user.sid === standardSid), false); }); await t.test("Windows PowerShell as OpenSSH DefaultShell is detected and works", async () => { await admin("New-ItemProperty HKLM:\\SOFTWARE\\OpenSSH -Name DefaultShell -Value \"$env:WINDIR\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -PropertyType String -Force | Out-Null"); profiles.add({ id: "ps", label: "win", host: HOST4, port: 22, username: ADMIN, authMethod: "password" }, ADMIN_PASSWORD); await manager.connect({ sessionId: "ps", profileId: "ps" }); const platform = await detectPlatform(manager, "ps"); assert.equal(platform.defaultShell, "powershell"); assert.ok((await api.inventory("ps")).users.length >= 3); await manager.disconnect("ps"); await admin(restoreShell); }); await t.test("deleting an account removes its profile and CH-J SSH rules", async () => { await api.sshPolicy("admin", { sid: testSid, policy: { ipv6: "deny" }, probeCredentials }); await admin(`$end=(Get-Date).AddSeconds(60); while ((Get-CimInstance Win32_UserProfile -Filter "SID='${testSid}'").Loaded -and (Get-Date) -lt $end) { Start-Sleep 2 }`); const result = await deleteAccount(testSid, USER, probeCredentials); assert.notEqual(result.result, "failed", JSON.stringify(result)); assert.equal(result.inventory.users.some((user) => user.sid === testSid), false); assert.equal(await admin(CONFIG_B64), originalConfig, "CH-J SSH rules of the deleted account were removed"); }); 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 () => { const { renderConfig } = require("../src/main/users/windowsSshPolicy"); const original = Buffer.from(originalConfig, "base64"); const content = renderConfig(original.toString("utf8"), new Map([["chjtst3", { access: "deny", password: "inherit", pubkey: "inherit", ipv4: "allow", ipv6: "allow" }]])); const sha = crypto.createHash("sha256").update(original).digest("hex"); const staged = await executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "ssh-stage", expected: sha, data: Buffer.from(content).toString("base64"), tuples: [] } }); const nonce = crypto.randomBytes(24).toString("hex"); const commit = await executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "ssh-commit", id: staged.id, nonce, expected: sha, candidateSha256: staged.candidateSha256, timeoutSeconds: 60 } }); assert.equal(commit.state, "awaiting-confirmation"); assert.match(await admin("Get-Content \"$env:ProgramData\\ssh\\sshd_config\" -Raw"), /DenyUsers chjtst3/, "the change is live"); await manager.disconnect("admin"); // simulates CH-J crashing before confirmation await new Promise((resolve) => setTimeout(resolve, 90000)); await manager.connect({ sessionId: "admin", profileId: "admin" }); assert.equal(await admin(CONFIG_B64), originalConfig, "restored without CH-J"); const status = await executor.run("admin", { script: REMOTE_SCRIPT, args: { op: "ssh-confirm", id: staged.id, nonce, confirm: false, waitSeconds: 1 } }); assert.equal(status.state, "rolled-back"); // A failing test login (wrong credentials) never confirms; the guard restores the previous file. await api.manage("admin", { action: "create", username: "chjtst3", password: TEST_PASSWORD }); const sid = (await api.inventory("admin")).users.find((user) => user.username === "chjtst3").sid; await assert.rejects(() => api.sshPolicy("admin", { sid, policy: { password: "no" }, probeCredentials: { password: "wrong-password" } }), { code: "USERS_SSH_PROBE_FAILED" }); if (!manager.sessions.get("admin")) await manager.connect({ sessionId: "admin", profileId: "admin" }); assert.equal(await admin(CONFIG_B64), originalConfig); await api.manage("admin", { action: "delete", sid, username: "chjtst3", removeProfile: true }); }); });