Publicerad källa
Ladda ner källa ZIP
CH-J Server Manager
Bläddra i kataloger och filer för en specifik programutgåva.
CH-J / Server Manager
377 filer
core-20261019T111616Z-160a00bfcddc
Tillagd: · Byggd:
1
"use strict";2
// End-to-end UI check of the chj.users plugin: real plugin runtime, preload, IPC, SSH session and3
// 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]5
const { app, BrowserWindow, ipcMain, protocol } = require("electron");6
const assert = require("node:assert/strict");7
const fs = require("node:fs");8
const os = require("node:os");9
const path = require("node:path");10
const { execFileSync, spawnSync } = require("node:child_process");11
const { hardenSession } = require("../src/main/bootstrap/createMainWindow");12
const { PluginRegistry } = require("../src/main/plugins/pluginRegistry");13
const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");14
const { SessionManager } = require("../src/main/sessions/sessionManager");15
const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");16
const { UserAdminService, createUserAdminApi } = require("../src/main/users/userAdminService");17
const { KeyGeneratorService } = require("../src/main/keys/keyGeneratorService");18
const { WindowsUserAdminService } = require("../src/main/users/windowsUserAdminService");19
const { WindowsExecutor } = require("../src/main/sessions/windowsPrivilegedExec");20
const { detectPlatform } = require("../src/main/sessions/platformDetect");22
const image = process.env.CHJ_USERS_IMAGE || "chj-users-it-debian";23
const container = `chj-users-ui-${process.pid}`;24
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-users-ui-"));25
const shots = process.argv.find((arg, index) => index > 1 && !arg.startsWith("-") && !arg.endsWith(".js")) || null;26
app.setPath("userData", root);27
protocol.registerSchemesAsPrivileged([{ scheme: "chj-plugin", privileges: { standard: true, secure: true, supportFetchAPI: true } }]);28
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));29
const errors = [];30
let window;31
const js = (source) => window.webContents.executeJavaScript(source);32
async 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")}`);36
}37
async 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());41
}42
function inContainer(command) {43
const result = spawnSync("docker", ["exec", container, "sh", "-c", command], { encoding: "utf8" });44
return { code: result.status, stdout: result.stdout };45
}46
async 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();`);49
}50
async function confirm() {51
await until("document.querySelector('#confirmDialog').open");52
await js("document.querySelector('#confirmForm').requestSubmit()");53
}54
async function openTab(name) { await js(`document.querySelector('#tab-${name}').click()`); }55
async 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")`);59
}61
async 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
const runtime = new PluginRuntime({ BrowserWindow, registry, sessionManager, userAdmin, keyGeneratorService,81
preload: path.join(__dirname, "../src/preload/pluginPreload.js"), getMainWindow: () => null, isVaultUnlocked: () => true });82
runtime.registerIpc(ipcMain);83
runtime.open("chj.users");84
window = runtime.windows.get("chj.users");85
window.webContents.on("console-message", (event) => { if (event.level === "error") errors.push(event.message); });86
window.setSize(1280, 860);87
await until("document.querySelector('button[data-username=\"admin\"]')");88
assert.match(await js("document.querySelector('#privilegeBadge').textContent"), /admin: sudo s heslem/);89
assert.equal(await js("document.querySelector('#elevatedButton').hidden"), false);90
assert.equal(await js("document.querySelectorAll('#sudoPassword').length"), 1, "only the dialog has a sudo password field");91
await shot("01-readonly-list");93
// Wrong password keeps the dialog usable; cancel leaves the plugin working.94
await js("document.querySelector('#elevatedButton').click()");95
await sudo("wrong-password");96
await until("!document.querySelector('#sudoError').hidden", 30000);97
await shot("02-sudo-wrong-password");98
await js("document.querySelector('#sudoDialog [data-close]').click()");99
await until("document.querySelector('#status').textContent.includes('zrušena')");100
await js("document.querySelector('#elevatedButton').click()");101
await sudo();102
await until("document.querySelector('#status').textContent.includes('Načteno') && document.querySelector('#elevatedButton').hidden");103
assert.match(await js("document.querySelector('#usersBody').textContent"), /plné sudo/);104
await shot("03-privileged-list");106
// Create an account through the dialog.107
await js("document.querySelector('#createButton').click()");108
await until("document.querySelector('#createDialog').open");109
await js(`document.querySelector('#createUsername').value = 'uitester'; document.querySelector('#createComment').value = 'UI Tester';110
document.querySelector('#createPassword').value = 'Ui-Pass-123'; document.querySelector('#createPassword2').value = 'Ui-Pass-123';`);111
await shot("04-create-dialog");112
await js("document.querySelector('#createForm').requestSubmit()");113
await sudo();114
await until("document.querySelector('#detailTitle').textContent === 'uitester'", 30000);115
assert.equal(inContainer("getent passwd uitester | cut -d: -f5").stdout.trim(), "UI Tester");116
assert.equal(await js("document.querySelector('#createPassword').value"), "", "password fields are cleared");118
// Load privileged detail, inspect all tabs.119
await selectUser("uitester");120
await js("Array.from(document.querySelectorAll('#detailNotice button')).find(b => b.textContent.includes('sudo')).click()");121
await sudo();122
await until("document.querySelector('#detailBadges').textContent.includes('ověřeno se sudo')", 30000);123
await shot("05-detail-account");124
await openTab("groups"); await shot("06-detail-groups");125
await openTab("ssh");126
assert.match(await js("document.querySelector('#panel-ssh').textContent"), /Skutečně IPv4/);127
await shot("07-detail-ssh");129
// Apply an SSH policy: password off, IPv6 denied.130
await js(`document.querySelector('input[name="policy-password"][value="no"]').click(); document.querySelector('input[name="policy-ipv6"][value="deny"]').click();131
document.querySelector('.policy-form').requestSubmit();`);132
await confirm();133
await sudo();134
await until("document.querySelector('#status').textContent.includes('SSH přístup byl změněn')", 60000);135
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/);136
await until("document.querySelector('button[data-username=\"uitester\"]').closest('tr').textContent.includes('bez IPv6')");137
assert.equal(await js("document.querySelector('#panel-account').hidden"), true, "only the selected tab panel is visible");138
await shot("08-ssh-applied");140
// Add a key via the dialog, using the local generator (private key saved locally only).141
await openTab("keys");142
await js("Array.from(document.querySelectorAll('#panel-keys button')).find(b => b.textContent.includes('Přidat Ed25519')).click()");143
await until("document.querySelector('#keyDialog').open");144
await js("document.querySelector('#keyGenerateButton').click(); document.querySelector('#keyGenRun').click();");145
await until("document.querySelector('#keyText').value.startsWith('ssh-ed25519 ')");146
await shot("09-key-dialog");147
await js("document.querySelector('#keyForm').requestSubmit()");148
await sudo();149
await until("document.querySelector('#panel-keys').textContent.includes('SHA256:')", 30000);150
assert.ok(fs.existsSync(keyPath), "private key stored locally");151
assert.equal(inContainer(`grep -c ssh-ed25519 /home/uitester/.ssh/authorized_keys`).stdout.trim(), "1");152
assert.equal(inContainer(`grep -c PRIVATE /home/uitester/.ssh/authorized_keys`).stdout.trim(), "0");153
await shot("10-keys");154
await openTab("diagnostics"); await shot("11-diagnostics");156
// root: protected account, but SSH access (incl. PermitRootLogin) and keys are manageable.157
await selectUser("root");158
assert.equal(await js("Array.from(document.querySelectorAll('#panel-account button')).some(b => b.textContent.includes('Odstranit'))"), false, "no destructive root actions");159
assert.match(await js("document.querySelector('#detailNotice').textContent"), /Chráněný systémový účet root/);160
await js("Array.from(document.querySelectorAll('#detailNotice button')).find(b => b.textContent.includes('sudo')).click()");161
await sudo();162
await until("document.querySelector('#detailBadges').textContent.includes('ověřeno se sudo')", 30000);163
await openTab("ssh");164
assert.match(await js("document.querySelector('#panel-ssh').textContent"), /PermitRootLogin \(root\)[\s\S]*Jen klíč \(doporučeno\)/);165
assert.equal(await js("document.querySelector('input[name=\"policy-permitRootLogin\"]').disabled"), false);166
await shot("11b-root-ssh");167
await openTab("keys");168
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");169
await shot("11c-root-keys");171
// Protected account: the session user cannot be deleted from the UI.172
await selectUser("admin");173
assert.equal(await js("Array.from(document.querySelectorAll('#panel-account button')).some(b => b.textContent.includes('Odstranit'))"), false);174
assert.match(await js("document.querySelector('#detailNotice').textContent"), /Tímto účtem je CH-J právě připojeno/);176
// Delete with typed confirmation.177
await selectUser("uitester");178
await openTab("account");179
await js("Array.from(document.querySelectorAll('#panel-account button')).find(b => b.textContent.includes('Odstranit')).click()");180
await until("document.querySelector('#confirmDialog').open");181
await js("document.querySelector('#confirmForm').requestSubmit()");182
assert.equal(await js("document.querySelector('#confirmDialog').open"), true, "typed confirmation required");183
await js("document.querySelector('#confirmTyped').value = 'uitester'; document.querySelector('#confirmForm').requestSubmit();");184
await sudo();185
await until("!document.querySelector('button[data-username=\"uitester\"]')", 60000);186
assert.equal(inContainer("getent passwd uitester").code, 2);187
assert.equal(inContainer("test -e /etc/ssh/sshd_config.d/01-chj-users.conf").code, 1, "SSH policy of the deleted account removed");188
await shot("12-after-delete");189
assert.deepEqual(errors, [], "no renderer errors");190
console.log("chj.users UI smoke passed");191
}193
run().then(() => { cleanup(); app.exit(0); }, (error) => { console.error(error); cleanup(); app.exit(1); });194
function cleanup() {195
try { execFileSync("docker", ["rm", "-f", container], { stdio: "ignore" }); } catch {}196
fs.rmSync(root, { recursive: true, force: true });197
}SHA-256
da92fa9292063eaf68ac9119467a1626e9ca841792e1d5b0c46b505176ab4c0dCH-J Proprietary Software License 1.14 · Källan finns under CH-J Proprietary Software License 1.14. Dess tillgänglighet ändrar inte licensvillkoren eller ger ytterligare rättigheter.
Arkiv SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a