CH-J Server Managerupravljanje strežnikov prek SSH
Meni
Objavljen vir

CH-J Server Manager

Brskajte po imenikih in datotekah za določeno izdajo programa.

Prenos vir ZIP
CH-J / Server Manager 377 datoteke
core-20261019T111616Z-160a00bfcddc Dodano: · Zgrajena:
usersWindowsIntegration.test.js Dodano: Datum, ki ni zabeležen 26,9 KB · 341 vrstice
Oznaka Prenesi datoteko
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 restores
5// 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=admin
7// CHJ_USERS_WINDOWS_PASSWORD=... CHJ_USERS_WINDOWS_FINGERPRINT=<base64 SHA256 host key> node --test test/usersWindowsIntegration.test.js
8// Optional: CHJ_USERS_WINDOWS_SLOW=1 also runs the 2-minute rollback-guard scenarios.
9const test = require("node:test");
10const assert = require("node:assert/strict");
11const crypto = require("node:crypto");
12const { Client } = require("ssh2");
13const { SessionManager } = require("../src/main/sessions/sessionManager");
14const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");
15const { WindowsExecutor, encodedCommand } = require("../src/main/sessions/windowsPrivilegedExec");
16const { detectPlatform } = require("../src/main/sessions/platformDetect");
17const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService");
18const { WindowsUserAdminService, REMOTE_SCRIPT } = require("../src/main/users/windowsUserAdminService");
19const { KeyGeneratorService } = require("../src/main/keys/keyGeneratorService");
21const env = process.env;
22const enabled = Boolean(env.CHJ_USERS_WINDOWS_HOST && env.CHJ_USERS_WINDOWS_PASSWORD && env.CHJ_USERS_WINDOWS_FINGERPRINT);
23const skip = enabled ? false : "Set CHJ_USERS_WINDOWS_* to run destructive tests on an isolated Windows test machine.";
24const HOST4 = env.CHJ_USERS_WINDOWS_HOST;
25const HOST6 = env.CHJ_USERS_WINDOWS_HOST6;
26const ADMIN = env.CHJ_USERS_WINDOWS_USER || "codex";
27const ADMIN_PASSWORD = env.CHJ_USERS_WINDOWS_PASSWORD;
28const FINGERPRINT_HEX = enabled ? Buffer.from(`${env.CHJ_USERS_WINDOWS_FINGERPRINT}=`, "base64").toString("hex") : "";
29const TEST_PASSWORD = `Chj-${crypto.randomBytes(6).toString("hex")}-9!`;
30const STANDARD_PASSWORD = `Chj-${crypto.randomBytes(6).toString("hex")}-7!`;
31const USER = "chjtst1";
32const STANDARD = "chjtst2";
34function 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 });
51class 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() {}
61let 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.
63async 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 }
72async 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();
80const CONFIG_B64 = "[Convert]::ToBase64String([IO.File]::ReadAllBytes(\"$env:ProgramData\\ssh\\sshd_config\"))";
81const 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)";
83test("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 = originalShell
97 ? `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 5
105 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 confirmation
328 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-25650cd036527497f9ec988a2f21d861b6849be4ff0d4763a692e1aaa990fb2d9f2

CH-J Proprietary Software License 1.14 · Vir je na voljo pod CH-J Proprietary Software License 1.14. Njena razpoložljivost ne spreminja pogojev licence ali dodeljuje dodatnih pravic.

Arhiv SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
Datoteke