CH-J Server ManagerSSH के ज़रिए सर्वर प्रबंधन
मेनू
प्रकाशित सोर्स

CH-J Server Manager

किसी विशेष ऐप्लिकेशन रिलीज़ की डायरेक्टरी और फ़ाइलें देखें।

सोर्स ZIP डाउनलोड करें
CH-J / Server Manager 377 फ़ाइलें
core-20261019T111616Z-160a00bfcddc जोड़ा गया: · बिल्ड किया गया:
usersWindows.test.js जोड़ा गया: तारीख दर्ज नहीं 24.6 KB · 291 पंक्तियाँ
कोड फ़ाइल डाउनलोड करें
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const windowsPolicy = require("../src/main/users/windowsSshPolicy");
6const { normalizePolicy, parseEffective, evaluateAccess } = require("../src/main/users/sshPolicy");
7const { BOOTSTRAP, bootstrapInput, encodedCommand, parseWindowsOutput } = require("../src/main/sessions/windowsPrivilegedExec");
8const { detectPlatform, shellName } = require("../src/main/sessions/platformDetect");
9const { WindowsUserAdminService, privilegeFromToken, validateWindowsName } = require("../src/main/users/windowsUserAdminService");
10const { UserAdminService } = require("../src/main/users/userAdminService");
12// Default sshd_config of Windows OpenSSH 9.5 (comments shortened), CRLF line endings as shipped.
13const WINDOWS_CONFIG = [
14 "# This is the sshd server system-wide configuration file.",
15 "#PasswordAuthentication yes",
16 "AuthorizedKeysFile\t.ssh/authorized_keys",
17 "Subsystem\tsftp\tsftp-server.exe",
18 "",
19 "Match Group administrators",
20 " AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys",
21 ""
22].join("\r\n");
23const ok = (value) => `#< CLIXML\r\nCHJ_RESULT:${Buffer.from(JSON.stringify(value)).toString("base64")}\r\n`;
24const err = (code, message) => `CHJ_ERR:${code}:${Buffer.from(message).toString("base64")}\r\n`;
26test("Windows sshd_config sections: DenyUsers before the first Match, Match User at the end, no KbdInteractive", () => {
27 const policies = new Map([
28 ["alice", normalizePolicy({ password: "no", ipv6: "deny" })],
29 ["bob", normalizePolicy({ access: "deny" })],
30 ["carol", normalizePolicy({})]
31 ]);
32 const text = windowsPolicy.renderConfig(WINDOWS_CONFIG, policies);
33 const lines = text.split("\r\n");
34 const globalIndex = lines.indexOf(windowsPolicy.GLOBAL_BEGIN);
35 const adminMatch = lines.indexOf("Match Group administrators");
36 assert.ok(globalIndex >= 0 && globalIndex < adminMatch, "global directives never follow an existing Match block");
37 assert.ok(lines.indexOf("DenyUsers alice@*:*") > globalIndex && lines.indexOf("DenyUsers alice@*:*") < adminMatch, "IPv6 rule without CIDR slash");
38 assert.doesNotMatch(text, /DenyUsers [^\r\n]*\//, "no slash: Windows OpenSSH rewrites it to a backslash");
39 assert.ok(lines.indexOf("DenyUsers bob") < adminMatch);
40 assert.ok(lines.indexOf("Match User alice") > adminMatch, "per-user Match blocks keep the lower priority");
41 assert.doesNotMatch(text, /KbdInteractiveAuthentication/);
42 assert.doesNotMatch(text, /carol/);
43 assert.doesNotMatch(text, /[^\r]\n/, "CRLF line endings are preserved");
44 const parsed = windowsPolicy.parseManagedPolicies(text);
45 assert.equal(parsed.consistent, true);
46 assert.deepEqual([...parsed.policies.keys()].sort(), ["alice", "bob"]);
47 assert.equal(windowsPolicy.renderConfig(text, new Map()), WINDOWS_CONFIG, "removing all policies restores the original bytes");
48 assert.equal(windowsPolicy.renderConfig(WINDOWS_CONFIG, new Map()), WINDOWS_CONFIG);
49});
51test("Windows sshd_config edits are refused for manual edits, damaged markers, Include and misplaced sections", () => {
52 const text = windowsPolicy.renderConfig(WINDOWS_CONFIG, new Map([["alice", normalizePolicy({ ipv4: "deny" })]]));
53 const edited = text.replace("DenyUsers alice@*.*.*.*", "DenyUsers alice@*.*.*.* mallory");
54 assert.equal(windowsPolicy.parseManagedPolicies(edited).consistent, false);
55 assert.throws(() => windowsPolicy.renderConfig(edited, new Map()), { code: "USERS_SSH_MANAGED_FILE_MODIFIED" });
56 assert.throws(() => windowsPolicy.renderConfig(text.replace(windowsPolicy.GLOBAL_BEGIN, "# x"), new Map()), { code: "USERS_SSH_LAYOUT_UNSUPPORTED" });
57 assert.throws(() => windowsPolicy.renderConfig(`Include C:/ProgramData/ssh/extra.conf\r\n${WINDOWS_CONFIG}`, new Map([["alice", normalizePolicy({ access: "deny" })]])), { code: "USERS_SSH_LAYOUT_UNSUPPORTED" });
58 const misplaced = `${WINDOWS_CONFIG}${windowsPolicy.GLOBAL_BEGIN}\r\n# END CH-J chj.users global\r\n`;
59 assert.equal(windowsPolicy.analyzeConfig(misplaced).supported, false);
60 assert.equal(windowsPolicy.renderConfig("PasswordAuthentication yes\n", new Map([["alice", normalizePolicy({ password: "no" })]])),
61 `PasswordAuthentication yes\n${windowsPolicy.GLOBAL_BEGIN}\n# chj-policy alice access=allow password=no pubkey=inherit ipv4=allow ipv6=allow\n# END CH-J chj.users global\n${windowsPolicy.MATCH_BEGIN}\nMatch User alice\n\tPasswordAuthentication no\n# END CH-J chj.users match\n`);
62 assert.equal(windowsPolicy.sshName("Codex"), "codex");
63 assert.equal(windowsPolicy.sshName("Jan Novák"), null, "names with spaces or non-ASCII are not written to sshd_config");
64});
66test("Windows effective evaluation ignores keyboard-interactive, which Windows OpenSSH does not support", () => {
67 const dump = parseEffective("passwordauthentication no\nkbdinteractiveauthentication yes\npubkeyauthentication yes\ndenyusers alice@*.*.*.*\ndenyusers bob@*:*");
68 assert.equal(evaluateAccess(dump, { username: "bob", address: "192.0.2.1", method: "password", keyboardInteractive: false }).methodAllowed, false);
69 assert.equal(evaluateAccess(dump, { username: "bob", address: "192.0.2.1", method: "password" }).methodAllowed, true, "Linux semantics unchanged");
70 assert.equal(evaluateAccess(dump, { username: "alice", address: "192.0.2.1", keyboardInteractive: false }).allowed, false);
71 assert.equal(evaluateAccess(dump, { username: "alice", address: "2001:db8::1", keyboardInteractive: false }).allowed, true);
72 assert.equal(evaluateAccess(dump, { username: "bob", address: "2001:db8::1", keyboardInteractive: false }).allowed, false);
73 assert.equal(evaluateAccess(dump, { username: "bob", address: "192.0.2.1", keyboardInteractive: false }).reasons.length, 0);
74 assert.equal(evaluateAccess(dump, { username: "alice", address: "192.0.2.1" }).reasons[0].managed, true);
75});
77test("PowerShell transport: short encoded bootstrap, structured stdin, no user data in the command line", () => {
78 const command = encodedCommand(BOOTSTRAP);
79 assert.ok(command.length < 8191, "fits the cmd.exe command-line limit");
80 assert.match(command, /^powershell\.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand [A-Za-z0-9+/=]+$/);
81 assert.match(Buffer.from(command.split(" ").pop(), "base64").toString("utf16le"), /\[Console\]::In/);
82 const input = bootstrapInput("script", { op: "account", password: "S3cret;& del *", name: "a\"b" });
83 const [script, args] = input.trim().split("\n").map((line) => Buffer.from(line, "base64").toString("utf8"));
84 assert.equal(script, "script");
85 assert.deepEqual(JSON.parse(args), { op: "account", password: "S3cret;& del *", name: "a\"b" });
86 assert.equal(command.includes("S3cret"), false);
87});
89test("PowerShell output parsing: results, error codes, cmd.exe failures and restricted PowerShell", () => {
90 assert.deepEqual(parseWindowsOutput(ok({ users: [{ name: "Jiří" }] }), "<Objs>progress</Objs>", 0), { users: [{ name: "Jiří" }] });
91 assert.throws(() => parseWindowsOutput(err("WINDOWS_ELEVATION_REQUIRED", "Token není zvýšený"), "", 0), { code: "USERS_WINDOWS_ELEVATION_REQUIRED", message: "Token není zvýšený" });
92 assert.throws(() => parseWindowsOutput("", "'powershell.exe' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n", 1), { code: "USERS_POWERSHELL_UNAVAILABLE" });
93 assert.throws(() => parseWindowsOutput("", "Cannot invoke method. Method invocation is supported only on core types in this language mode.", 1), { code: "USERS_POWERSHELL_RESTRICTED" });
94 assert.throws(() => parseWindowsOutput("garbage", "", 0), { code: "USERS_PROTOCOL_ERROR" });
95 assert.throws(() => parseWindowsOutput("CHJ_RESULT:not-base64-json", "", 0), { code: "USERS_PROTOCOL_ERROR" });
96});
98function fakeManager(responses) {
99 const manager = { calls: [], record: { sessionId: "s", platform: null } };
100 manager._requireConnected = () => manager.record;
101 manager._execStream = (_record, command) => {
102 manager.calls.push(command);
103 const response = responses.shift();
104 return { done: response instanceof Error ? Promise.reject(response) : Promise.resolve({ code: 0, stdout: "", stderr: "", ...response }) };
105 };
106 return manager;
109test("platform detection: uname for POSIX, Win32_OperatingSystem for Windows (cmd.exe, Git Bash uname), unknown otherwise", async () => {
110 let manager = fakeManager([{ code: 0, stdout: "Linux\n" }]);
111 assert.equal((await detectPlatform(manager, "s")).os, "linux");
112 assert.equal(manager.calls.length, 1);
113 const windowsInfo = { caption: "Microsoft Windows Server 2025 Datacenter", version: "10.0.26100", build: "26100", productType: 3, installationType: "Server Core", defaultShell: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", psVersion: "5.1.26100.1", is64BitProcess: true, languageMode: "FullLanguage", localAccounts: true, pwsh: "" };
114 manager = fakeManager([{ code: 1, stderr: "'uname' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n" }, { stdout: ok(windowsInfo) }]);
115 const server = await detectPlatform(manager, "s");
116 assert.equal(server.os, "windows"); assert.equal(server.serverCore, true); assert.equal(server.server, true); assert.equal(server.defaultShell, "powershell");
117 assert.match(manager.calls[1], /-EncodedCommand /);
118 manager = fakeManager([{ code: 0, stdout: "MINGW64_NT-10.0-26200\n" }, { stdout: ok({ ...windowsInfo, productType: 1, installationType: "Client", defaultShell: "" }) }]);
119 const client = await detectPlatform(manager, "s");
120 assert.equal(client.os, "windows", "Git for Windows uname does not make the host Linux"); assert.equal(client.defaultShell, "cmd.exe");
121 manager = fakeManager([{ code: 127, stderr: "sh: uname: not found" }, { code: 127, stderr: "sh: powershell.exe: not found" }]);
122 assert.equal((await detectPlatform(manager, "s")).os, "unknown");
123 assert.equal(shellName("C:\\Program Files\\PowerShell\\7\\pwsh.exe"), "pwsh");
124 assert.equal(shellName("C:\\Windows\\System32\\cmd.exe"), "cmd.exe");
125});
127test("Windows token states: elevated, Administrators member with filtered token, standard user", () => {
128 assert.equal(privilegeFromToken({ elevated: true, adminMember: true, name: "PC\\admin", sid: "S-1-5-21-1-2-3-1001" }, { sshClient: "192.0.2.7 50000 22" }).mode, "windows-elevated");
129 const filtered = privilegeFromToken({ elevated: false, adminMember: true, adminDenyOnly: true, name: "PC\\admin" }, { sshClient: "2001:db8::7 50000 22" });
130 assert.equal(filtered.mode, "windows-admin-unelevated");
131 assert.equal(filtered.clientAddress, "2001:db8::7");
132 assert.equal(privilegeFromToken({ elevated: false, adminMember: false }, {}).mode, "windows-standard");
133 assert.equal(filtered.sudoInstalled, false);
134});
136const MACHINE = "S-1-5-21-3493863762-3532507802-4093449623";
137function inventoryFixture({ elevated = true } = {}) {
138 const user = (name, rid, extra = {}) => ({ name, sid: `${MACHINE}-${rid}`, enabled: true, fullName: "", description: "", source: "Local", lastLogon: null, passwordLastSet: "2026-10-01T10:00:00.0000000Z", passwordRequired: true, passwordExpires: null, accountExpires: null, userMayChangePassword: true, ...extra });
139 return {
140 system: { caption: "Microsoft Windows 11 Pro", version: "10.0.26200", displayVersion: "25H2", installationType: "Client", productType: 1, partOfDomain: true, domain: "corp.example", psVersion: "5.1.26100.9711", psEdition: "Desktop", is64BitProcess: true, sshClient: "192.0.2.142 51000 22" },
141 token: { name: "WIN-TEST\\admin", sid: `${MACHINE}-1002`, elevated, adminMember: true, integrity: elevated ? "high" : "medium" },
142 users: [user("Administrator", 500, { enabled: false }), user("Guest", 501, { enabled: false }), user("codex", 1002), user("novak", 1004, { fullName: "Jan Novák" }), user("yosef", 1001, { source: "MicrosoftAccount" })],
143 // Localized (Czech) group names: Administrators must be identified by SID only.
144 groups: [
145 { sid: "S-1-5-32-544", name: "Administrátoři", members: [{ sid: `${MACHINE}-500`, name: "PC\\Administrator" }, { sid: `${MACHINE}-1002`, name: "PC\\codex" }, { sid: "S-1-5-21-111-222-333-1105", name: "CORP\\ops", source: "ActiveDirectory" }] },
146 { sid: "S-1-5-32-545", name: "Uživatelé", members: [{ sid: `${MACHINE}-1004`, name: "PC\\novak" }, { sid: `${MACHINE}-1002`, name: "PC\\codex" }] },
147 { sid: "S-1-5-32-555", name: "Uživatelé vzdálené plochy", members: [] }
148 ],
149 ssh: { installed: true, version: "OpenSSH_for_Windows_9.5p2", service: "Running", configReadable: true, configSha256: "abc", configData: Buffer.from(WINDOWS_CONFIG).toString("base64") }
150 };
153function service(runs, { alive = true } = {}) {
154 const calls = [];
155 const executor = { run: async (_sessionId, { args }) => { calls.push(args); const next = runs[args.op]; if (typeof next === "function") return next(args); if (next instanceof Error) throw next; return next; } };
156 const sessionManager = {
157 sessionAuthInfo: () => ({ username: "codex", authMethod: "password", publicKeyFingerprint: null }),
158 probeLoginRequirements: (_id, credentials) => credentials.password ? { ready: true } : { ready: false, missing: "password" },
159 connectionTarget: () => ({ profileId: "p", address: "192.0.2.143" }),
160 _requireConnected: () => { if (!alive) throw Object.assign(new Error("gone"), { code: "SESSION_NOT_CONNECTED" }); return {}; },
161 freshExec: async () => { throw Object.assign(new Error("A new SSH login was not confirmed: auth failed"), { code: "SSH_PROBE_FAILED" }); }
162 };
163 return { svc: new WindowsUserAdminService({ sessionManager, windowsExecutor: executor }), calls, sessionManager };
166test("Windows inventory: SID identity, Administrators by SID with localized names, protected and external identities", async () => {
167 const { svc } = service({ inventory: inventoryFixture() });
168 const inventory = await svc.inventory("s", { defaultShell: "cmd.exe" });
169 const byName = Object.fromEntries(inventory.users.map((user) => [user.username, user]));
170 assert.equal(inventory.platform, "windows");
171 assert.equal(byName.codex.admin, true);
172 assert.deepEqual(byName.codex.groups.sort(), ["Administrátoři", "Uživatelé"].sort());
173 assert.equal(byName.novak.admin, false);
174 assert.equal(byName.Administrator.protection, "builtin");
175 assert.equal(byName.codex.protection, "session");
176 assert.equal(byName.novak.protection, null);
177 assert.equal(byName.novak.uid, undefined, "no invented Linux UID");
178 assert.equal(byName.novak.sudo, undefined, "Windows administrators are not presented as sudo");
179 assert.equal(inventory.externalIdentities.length, 1);
180 assert.equal(inventory.externalIdentities[0].name, "CORP\\ops");
181 assert.equal(inventory.server.partOfDomain, true);
182 assert.equal(inventory.privileged, true);
183 assert.equal(inventory.server.sshLayout, "ok");
184});
186test("Windows management refuses without an elevated token, for protected accounts, Linux-only actions and bad input", async () => {
187 const unelevated = service({ probe: { token: inventoryFixture().token && { ...inventoryFixture({ elevated: false }).token }, system: inventoryFixture().system } });
188 await assert.rejects(() => unelevated.svc.manage("s", { action: "disable", sid: `${MACHINE}-1004` }), { code: "USERS_WINDOWS_ELEVATION_REQUIRED", message: /UAC/ });
189 assert.deepEqual(unelevated.calls.map((call) => call.op), ["probe"], "nothing is attempted without elevation");
190 const elevated = service({ probe: { token: inventoryFixture().token, system: inventoryFixture().system }, inventory: inventoryFixture(), account: { result: "ok", steps: [] } });
191 await assert.rejects(() => elevated.svc.manage("s", { action: "disable", sid: `${MACHINE}-500` }), { code: "USERS_PROTECTED_USER" });
192 await assert.rejects(() => elevated.svc.manage("s", { action: "delete", sid: `${MACHINE}-1002` }), { code: "USERS_PROTECTED_USER" });
193 await assert.rejects(() => elevated.svc.manage("s", { action: "setShell", sid: `${MACHINE}-1004`, shell: "/bin/sh" }), { code: "USERS_NOT_SUPPORTED_ON_PLATFORM" });
194 await assert.rejects(() => elevated.svc.manage("s", { action: "disable", sid: "S-1-5-21-1-2-3-4;Remove-LocalUser" }), { code: "USERS_SID_INVALID" });
195 await assert.rejects(() => elevated.svc.manage("s", { action: "addGroup", sid: `${MACHINE}-1004`, group: "Administrators" }), { code: "USERS_GROUP_INVALID" });
196 await assert.rejects(() => elevated.svc.manage("s", { action: "setPassword", sid: `${MACHINE}-1004`, password: "a\r\nb" }), { code: "USERS_PASSWORD_INVALID" });
197 await elevated.svc.manage("s", { action: "grantAdmin", sid: `${MACHINE}-1004` });
198 const call = elevated.calls.find((entry) => entry.op === "account");
199 assert.deepEqual(call, { op: "account", action: "admin-grant", sid: `${MACHINE}-1004` });
200 for (const name of ["a;b", "x$(y)", "a|b", "a\"b", "bad/name", "toolong_abcdefghijklm", "end.", "mid\nline"]) assert.throws(() => validateWindowsName(name), { code: "USERS_NAME_INVALID" }, name);
201 assert.equal(validateWindowsName("Jan Novak"), "Jan Novak");
202});
204function detailFixture(name = "novak", rid = 1004, groups = [{ sid: "S-1-5-32-545", name: "Uživatelé" }]) {
205 const effective = (extra = "") => ({ code: 0, text: `passwordauthentication yes\nkbdinteractiveauthentication yes\npubkeyauthentication yes\nauthorizedkeysfile .ssh/authorized_keys\nlistenaddress 0.0.0.0:22\nlistenaddress [::]:22${extra}` });
206 return {
207 user: { name, sid: `${MACHINE}-${rid}`, enabled: true, fullName: "", description: "", source: "Local" },
208 groups, profilePath: `C:\\Users\\${name}`, token: inventoryFixture().token, system: inventoryFixture().system,
209 ssh: inventoryFixture().ssh, syntax: { code: 0, output: "" }, effective4: effective(), effective6: effective(),
210 keyFiles: [{ spec: ".ssh/authorized_keys", path: `C:\\Users\\${name}\\.ssh\\authorized_keys`, shared: false, type: "absent", dirExists: true, reparse: false, acl: [] }]
211 };
214test("Windows detail: effective IPv4/IPv6 state, shared administrators key file and unsafe ACL warnings", async () => {
215 const admin = detailFixture("codex", 1002, [{ sid: "S-1-5-32-544", name: "Administrátoři" }]);
216 admin.keyFiles = [{ spec: "__PROGRAMDATA__/ssh/administrators_authorized_keys", path: "C:\\ProgramData\\ssh\\administrators_authorized_keys", shared: true, type: "file", reparse: false, sha256: "x", data: "", acl: [{ sid: "S-1-5-18", rights: "FullControl", type: "Allow" }, { sid: "S-1-5-32-544", rights: "FullControl", type: "Allow" }, { sid: "S-1-5-11", rights: "Modify, Synchronize", type: "Allow" }] }];
217 const { svc } = service({ probe: { token: inventoryFixture().token, system: inventoryFixture().system }, detail: admin });
218 const detail = await svc.detail("s", { sid: `${MACHINE}-1002` }, {});
219 assert.equal(detail.account.admin, true);
220 assert.equal(detail.self, true);
221 assert.equal(detail.keys.files[0].shared, true);
222 assert.ok(detail.ssh.warnings.some((warning) => /share one administrators_authorized_keys/.test(warning)));
223 assert.ok(detail.ssh.warnings.some((warning) => /S-1-5-11 has Modify/.test(warning)));
224 assert.equal(detail.ssh.effective.ipv4.methods.password, true);
225 assert.equal(detail.ssh.layoutSupported, true);
226 assert.equal(Object.keys(detail).some((key) => key.startsWith("_") && key !== "_keyFiles" && key !== "_config" && key !== "_addresses" && key !== "_groups"), false);
227});
229test("Windows SSH policy: existing Match precedence, lockout and missing test-login credentials stop before any change", async () => {
230 const stageWith = (afterExtra) => (args) => ({
231 id: "a".repeat(32), syntaxCode: 0, syntaxOutput: "", candidateSha256: "c",
232 before: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: "passwordauthentication yes\npubkeyauthentication yes" }])),
233 after: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: afterExtra(tuple) }]))
234 });
235 const base = { probe: { token: inventoryFixture().token, system: inventoryFixture().system }, detail: detailFixture() };
236 let fixture = service({ ...base, "ssh-stage": stageWith(() => "passwordauthentication yes\npubkeyauthentication yes") });
237 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" }, probeCredentials: { password: "x" } }), { code: "USERS_SSH_POLICY_NOT_EFFECTIVE" });
238 assert.equal(fixture.calls.some((call) => call.op === "ssh-commit"), false, "nothing is committed");
239 fixture = service(base);
240 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" } }), { code: "USERS_SSH_PROBE_CREDENTIALS_REQUIRED" });
241 assert.equal(fixture.calls.some((call) => call.op === "ssh-stage"), false);
242 fixture = service({ ...base, "ssh-stage": stageWith((tuple) => tuple.user === "codex" ? "passwordauthentication yes\npubkeyauthentication yes\ndenyusers codex@0.0.0.0/0" : "passwordauthentication no\npubkeyauthentication yes") });
243 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" }, probeCredentials: { password: "x" } }), { code: "USERS_SSH_LOCKOUT_PREVENTED" });
244 assert.equal(fixture.calls.some((call) => call.op === "ssh-commit"), false);
245});
247test("Windows SSH policy: a failing new login never confirms and the server-side guard rolls back", async () => {
248 const fixture = service({
249 probe: { token: inventoryFixture().token, system: inventoryFixture().system },
250 detail: detailFixture(),
251 "ssh-stage": (args) => ({ id: "b".repeat(32), syntaxCode: 0, syntaxOutput: "", candidateSha256: "c",
252 before: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: "passwordauthentication yes\npubkeyauthentication yes" }])),
253 after: Object.fromEntries(args.tuples.map((tuple) => [`${tuple.user}|${tuple.addr}`, { code: 0, text: tuple.user === "novak" ? "passwordauthentication no\npubkeyauthentication yes" : "passwordauthentication yes\npubkeyauthentication yes" }])) }),
254 "ssh-commit": { id: "b".repeat(32), state: "awaiting-confirmation", message: "sshd restarted" },
255 "ssh-confirm": (args) => ({ id: args.id, state: args.confirm ? "confirmed" : "rolled-back", message: "No confirmation from a new SSH login before the timeout." })
256 });
257 await assert.rejects(() => fixture.svc.sshPolicy("s", { sid: `${MACHINE}-1004`, policy: { password: "no" }, probeCredentials: { password: "wrong" } }), (error) => {
258 assert.equal(error.code, "USERS_SSH_PROBE_FAILED");
259 assert.match(error.message, /rolled-back/);
260 return true;
261 });
262 const ops = fixture.calls.map((call) => call.op);
263 assert.deepEqual(ops.slice(-3), ["ssh-stage", "ssh-commit", "ssh-confirm"]);
264 assert.equal(fixture.calls.at(-1).confirm, false, "the old session only waits for the guard and never confirms");
265 assert.match(fixture.calls.find((call) => call.op === "ssh-commit").nonce, /^[0-9a-f]{48}$/);
266});
268test("the shared users service routes Windows sessions to the Windows backend without touching sudo", async () => {
269 const calls = [];
270 const windowsService = new Proxy({}, { get: (_target, name) => async (...args) => { calls.push(name); return { platform: "windows", args: args.length }; } });
271 const linuxExecutor = { probe: async () => { throw new Error("sudo probe must not run on Windows"); } };
272 const sessionManager = { _requireConnected: () => ({}) };
273 const shared = new UserAdminService({ sessionManager, privilegedExecutor: linuxExecutor, windowsService, detectPlatform: async () => ({ os: "windows", defaultShell: "cmd.exe" }) });
274 for (const method of ["privileges", "inventory", "detail", "manage", "keys", "sshPolicy"]) await shared[method]("s", {});
275 assert.deepEqual(calls, ["privileges", "inventory", "detail", "manage", "keys", "sshPolicy"]);
276 const unknown = new UserAdminService({ sessionManager, privilegedExecutor: linuxExecutor, windowsService, detectPlatform: async () => ({ os: "unknown", reason: "no shell" }) });
277 await assert.rejects(() => unknown.inventory("s"), { code: "USERS_PLATFORM_UNSUPPORTED" });
278});
280test("Windows remote script keeps secrets in stdin, identifies Administrators by SID and arms rollback before restart", () => {
281 const script = require("node:fs").readFileSync(require("node:path").join(__dirname, "../src/main/users/windows/remoteUsers.ps1"), "utf8");
282 assert.match(script, /\$AdminsSid = 'S-1-5-32-544'/);
283 assert.doesNotMatch(script, /-Name '?Administrators'?|Get-LocalGroup -Name Administrators/i, "no localized group names");
284 assert.doesNotMatch(script, /KbdInteractiveAuthentication/);
285 assert.doesNotMatch(script, /\bsudo\b/);
286 assert.doesNotMatch(script, /Invoke-Expression|\biex\b/);
287 assert.match(script, /\[NullString\]::Value/, "File.Replace receives a real null backup path");
288 const apply = script.slice(script.indexOf("$ApplyScript = @'"));
289 assert.ok(apply.indexOf("Status 'armed'") < apply.indexOf("RestartSshd\n") || apply.indexOf("Status 'armed'") < apply.indexOf("try { RestartSshd }"), "rollback guard is armed before the restart");
290 assert.match(apply, /Restore 'No confirmation from a new SSH login before the timeout.'/);
291});
SHA-256ec0f348f2f5c840620142742571a4f818455390188939569593252d10e415d02

CH-J Proprietary Software License 1.14 · सोर्स CH-J Proprietary Software License 1.14 के तहत उपलब्ध कराया जाता है। इसकी उपलब्धता लाइसेंस की शर्तें नहीं बदलती और न ही अतिरिक्त अधिकार देती है।

आर्काइव SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
फ़ाइलें