CH-J ToolsSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Tools

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J / Server Manager 406 Dateien
core-20261024T135205Z-7c487d719de2 Hinzugefügt: · Erstellt:
smokeUsersUi.js Hinzugefügt: Datum nicht erfasst 13,1 KB · 198 Zeilen
Code Datei herunterladen
1"use strict";
2// End-to-end UI check of the chj.users plugin: real plugin runtime, preload, IPC, SSH session and
3// sudo flow against a disposable Docker container built from test/fixtures/usersIntegration.
4// Run: CHJ_USERS_IMAGE=chj-users-it-debian npx electron scripts/smokeUsersUi.js [screenshot-dir]
5const { app, BrowserWindow, ipcMain, protocol } = require("electron");
6const assert = require("node:assert/strict");
7const fs = require("node:fs");
8const os = require("node:os");
9const path = require("node:path");
10const { execFileSync, spawnSync } = require("node:child_process");
11const { hardenSession } = require("../src/main/bootstrap/createMainWindow");
12const { PluginRegistry } = require("../src/main/plugins/pluginRegistry");
13const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");
14const { SessionManager } = require("../src/main/sessions/sessionManager");
15const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");
16const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService");
17const { KeyGeneratorService } = require("../src/main/keys/keyGeneratorService");
18const { WindowsUserAdminService } = require("../src/main/users/windowsUserAdminService");
19const { WindowsExecutor } = require("../src/main/sessions/windowsPrivilegedExec");
20const { detectPlatform } = require("../src/main/sessions/platformDetect");
22const image = process.env.CHJ_USERS_IMAGE || "chj-users-it-debian";
23const container = `chj-users-ui-${process.pid}`;
24const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-users-ui-"));
25const shots = process.argv.find((arg, index) => index > 1 && !arg.startsWith("-") && !arg.endsWith(".js")) || null;
26app.setPath("userData", root);
27protocol.registerSchemesAsPrivileged([{ scheme: "chj-plugin", privileges: { standard: true, secure: true, supportFetchAPI: true } }]);
28const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
29const errors = [];
30let window;
31const js = (source) => window.webContents.executeJavaScript(source);
32async function until(source, timeout = 20000) {
33 const started = Date.now();
34 while (Date.now() - started < timeout) { if (await js(`(() => { try { return (${source}); } catch { return false; } })()`)) return; await delay(60); }
35 throw new Error(`UI condition timed out: ${source}\nstatus: ${await js("document.querySelector('#status').textContent")}`);
37async function shot(name) {
38 if (!shots) return;
39 fs.mkdirSync(shots, { recursive: true });
40 fs.writeFileSync(path.join(shots, `${name}.png`), (await window.webContents.capturePage()).toPNG());
42function inContainer(command) {
43 const result = spawnSync("docker", ["exec", container, "sh", "-c", command], { encoding: "utf8" });
44 return { code: result.status, stdout: result.stdout };
46async function sudo(password = "AdminPass-1") {
47 await until("document.querySelector('#sudoDialog').open");
48 await js(`document.querySelector('#sudoPassword').value = ${JSON.stringify(password)}; document.querySelector('#sudoForm').requestSubmit();`);
50async function confirm() {
51 await until("document.querySelector('#confirmDialog').open");
52 await js("document.querySelector('#confirmForm').requestSubmit()");
54async function openTab(name) { await js(`document.querySelector('#tab-${name}').click()`); }
55async function selectUser(name) {
56 await until(`document.querySelector('button[data-username="${name}"]')`);
57 await js(`document.querySelector('button[data-username="${name}"]').click()`);
58 await until(`document.querySelector('#detailTitle').textContent === "${name}" && !document.querySelector('#detailSubtitle').textContent.includes("Načítám")`);
61async function run() {
62 execFileSync("docker", ["run", "-d", "--rm", "--name", container, "--sysctl", "net.ipv6.conf.all.disable_ipv6=0", "-p", "127.0.0.1::22", image]);
63 const port = Number(execFileSync("docker", ["port", container, "22/tcp"], { encoding: "utf8" }).trim().split(":").pop());
64 await delay(1500);
65 await app.whenReady(); hardenSession();
66 const hostKeys = new Map();
67 const profileService = {
68 get: () => ({ id: "it", label: "Docker test", host: "127.0.0.1", port, username: "admin", authMethod: "password" }),
69 getHostKey: () => hostKeys.get(port) || null, trustHostKey: (_host, _port, fingerprint) => hostKeys.set(port, fingerprint),
70 getStoredPassword: () => "AdminPass-1", markUsed() {}
71 };
72 const sessionManager = new SessionManager({ profileService, latencyMonitor: { start: () => () => {} } });
73 try { await sessionManager.connect({ sessionId: "ui", profileId: "it" }); }
74 catch (error) { sessionManager.trustPending({ sessionId: "ui", profileId: "it", fingerprint: error.fingerprint }); await sessionManager.connect({ sessionId: "ui", profileId: "it" }); }
75 const windowsService = new WindowsUserAdminService({ sessionManager, windowsExecutor: new WindowsExecutor({ sessionManager }) });
76 const userAdmin = createUserAdminApi(new UserAdminService({ sessionManager, privilegedExecutor: new PrivilegedExecutor({ sessionManager }), windowsService, detectPlatform }));
77 const keyPath = path.join(root, "id_ed25519_ui");
78 const keyGeneratorService = new KeyGeneratorService({ selectSavePath: async () => ({ canceled: false, path: keyPath }) });
79 const registry = new PluginRegistry(root, null, { bundledRoot: path.join(__dirname, "../src/main/firstPartyPlugins") });
80 // The assertions below check the Czech UI texts.
81 const runtime = new PluginRuntime({ BrowserWindow, registry, getLanguage: () => "cs", sessionManager, userAdmin, keyGeneratorService,
82 preload: path.join(__dirname, "../src/preload/pluginPreload.js"), getMainWindow: () => null, isVaultUnlocked: () => true });
83 runtime.registerIpc(ipcMain);
84 runtime.open("chj.users");
85 window = runtime.windows.get("chj.users");
86 window.webContents.on("console-message", (event) => { if (event.level === "error") errors.push(event.message); });
87 window.setSize(1280, 860);
88 await until("document.querySelector('button[data-username=\"admin\"]')");
89 assert.match(await js("document.querySelector('#privilegeBadge').textContent"), /admin: sudo s heslem/);
90 assert.equal(await js("document.querySelector('#elevatedButton').hidden"), false);
91 assert.equal(await js("document.querySelectorAll('#sudoPassword').length"), 1, "only the dialog has a sudo password field");
92 await shot("01-readonly-list");
94 // Wrong password keeps the dialog usable; cancel leaves the plugin working.
95 await js("document.querySelector('#elevatedButton').click()");
96 await sudo("wrong-password");
97 await until("!document.querySelector('#sudoError').hidden", 30000);
98 await shot("02-sudo-wrong-password");
99 await js("document.querySelector('#sudoDialog [data-close]').click()");
100 await until("document.querySelector('#status').textContent.includes('zrušena')");
101 await js("document.querySelector('#elevatedButton').click()");
102 await sudo();
103 await until("document.querySelector('#status').textContent.includes('Načteno') && document.querySelector('#elevatedButton').hidden");
104 assert.match(await js("document.querySelector('#usersBody').textContent"), /plné sudo/);
105 await shot("03-privileged-list");
107 // Create an account through the dialog.
108 await js("document.querySelector('#createButton').click()");
109 await until("document.querySelector('#createDialog').open");
110 await js(`document.querySelector('#createUsername').value = 'uitester'; document.querySelector('#createComment').value = 'UI Tester';
111 document.querySelector('#createPassword').value = 'Ui-Pass-123'; document.querySelector('#createPassword2').value = 'Ui-Pass-123';`);
112 await shot("04-create-dialog");
113 await js("document.querySelector('#createForm').requestSubmit()");
114 await sudo();
115 await until("document.querySelector('#detailTitle').textContent === 'uitester'", 30000);
116 assert.equal(inContainer("getent passwd uitester | cut -d: -f5").stdout.trim(), "UI Tester");
117 assert.equal(await js("document.querySelector('#createPassword').value"), "", "password fields are cleared");
119 // Load privileged detail, inspect all tabs.
120 await selectUser("uitester");
121 await js("Array.from(document.querySelectorAll('#detailNotice button')).find(b => b.textContent.includes('sudo')).click()");
122 await sudo();
123 await until("document.querySelector('#detailBadges').textContent.includes('ověřeno se sudo')", 30000);
124 await shot("05-detail-account");
125 await openTab("groups"); await shot("06-detail-groups");
126 await openTab("ssh");
127 assert.match(await js("document.querySelector('#panel-ssh').textContent"), /Skutečně IPv4/);
128 await shot("07-detail-ssh");
130 // Apply an SSH policy: password off, IPv6 denied.
131 await js(`document.querySelector('input[name="policy-password"][value="no"]').click(); document.querySelector('input[name="policy-ipv6"][value="deny"]').click();
132 document.querySelector('.policy-form').requestSubmit();`);
133 await confirm();
134 await sudo();
135 await until("document.querySelector('#status').textContent.includes('SSH přístup byl změněn')", 60000);
136 assert.match(inContainer("cat /etc/ssh/sshd_config.d/01-chj-users.conf").stdout, /DenyUsers uitester@::\/0[\s\S]*Match User uitester\n\tPasswordAuthentication no/);
137 await until("document.querySelector('button[data-username=\"uitester\"]').closest('tr').textContent.includes('bez IPv6')");
138 assert.equal(await js("document.querySelector('#panel-account').hidden"), true, "only the selected tab panel is visible");
139 await shot("08-ssh-applied");
141 // Add a key via the dialog, using the local generator (private key saved locally only).
142 await openTab("keys");
143 await js("Array.from(document.querySelectorAll('#panel-keys button')).find(b => b.textContent.includes('Přidat Ed25519')).click()");
144 await until("document.querySelector('#keyDialog').open");
145 await js("document.querySelector('#keyGenerateButton').click(); document.querySelector('#keyGenRun').click();");
146 await until("document.querySelector('#keyText').value.startsWith('ssh-ed25519 ')");
147 await shot("09-key-dialog");
148 await js("document.querySelector('#keyForm').requestSubmit()");
149 await sudo();
150 await until("document.querySelector('#panel-keys').textContent.includes('SHA256:')", 30000);
151 assert.ok(fs.existsSync(keyPath), "private key stored locally");
152 assert.equal(inContainer(`grep -c ssh-ed25519 /home/uitester/.ssh/authorized_keys`).stdout.trim(), "1");
153 assert.equal(inContainer(`grep -c PRIVATE /home/uitester/.ssh/authorized_keys`).stdout.trim(), "0");
154 await shot("10-keys");
155 await openTab("diagnostics"); await shot("11-diagnostics");
157 // root: protected account, but SSH access (incl. PermitRootLogin) and keys are manageable.
158 await selectUser("root");
159 assert.equal(await js("Array.from(document.querySelectorAll('#panel-account button')).some(b => b.textContent.includes('Odstranit'))"), false, "no destructive root actions");
160 assert.match(await js("document.querySelector('#detailNotice').textContent"), /Chráněný systémový účet root/);
161 await js("Array.from(document.querySelectorAll('#detailNotice button')).find(b => b.textContent.includes('sudo')).click()");
162 await sudo();
163 await until("document.querySelector('#detailBadges').textContent.includes('ověřeno se sudo')", 30000);
164 await openTab("ssh");
165 assert.match(await js("document.querySelector('#panel-ssh').textContent"), /PermitRootLogin \(root\)[\s\S]*Jen klíč \(doporučeno\)/);
166 assert.equal(await js("document.querySelector('input[name=\"policy-permitRootLogin\"]').disabled"), false);
167 await shot("11b-root-ssh");
168 await openTab("keys");
169 assert.equal(await js("Array.from(document.querySelectorAll('#panel-keys button')).some(b => b.textContent.includes('Přidat Ed25519') && !b.disabled)"), true, "root keys can be added");
170 await shot("11c-root-keys");
172 // Protected account: the session user cannot be deleted from the UI.
173 await selectUser("admin");
174 assert.equal(await js("Array.from(document.querySelectorAll('#panel-account button')).some(b => b.textContent.includes('Odstranit'))"), false);
175 assert.match(await js("document.querySelector('#detailNotice').textContent"), /Tímto účtem je CH-J právě připojeno/);
177 // Delete with typed confirmation.
178 await selectUser("uitester");
179 await openTab("account");
180 await js("Array.from(document.querySelectorAll('#panel-account button')).find(b => b.textContent.includes('Odstranit')).click()");
181 await until("document.querySelector('#confirmDialog').open");
182 await js("document.querySelector('#confirmForm').requestSubmit()");
183 assert.equal(await js("document.querySelector('#confirmDialog').open"), true, "typed confirmation required");
184 await js("document.querySelector('#confirmTyped').value = 'uitester'; document.querySelector('#confirmForm').requestSubmit();");
185 await sudo();
186 await until("!document.querySelector('button[data-username=\"uitester\"]')", 60000);
187 assert.equal(inContainer("getent passwd uitester").code, 2);
188 assert.equal(inContainer("test -e /etc/ssh/sshd_config.d/01-chj-users.conf").code, 1, "SSH policy of the deleted account removed");
189 await shot("12-after-delete");
190 assert.deepEqual(errors, [], "no renderer errors");
191 console.log("chj.users UI smoke passed");
194run().then(() => { cleanup(); app.exit(0); }, (error) => { console.error(error); cleanup(); app.exit(1); });
195function cleanup() {
196 try { execFileSync("docker", ["rm", "-f", container], { stdio: "ignore" }); } catch {}
197 fs.rmSync(root, { recursive: true, force: true });
SHA-256140a83b75e5091b9ecee39c59148047ecd077d265adc6b79a648a336197892c6

CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

SHA-256 des Archivs476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
Dateien