sudo for firewall, network and Docker: try passwordless sudo first, then the saved host password, otherwise ask for the sudo password (up to 3 attempts, kept only for the open tab) instead of failing with "root privileges required"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -152,6 +152,10 @@
|
|||||||
'Command is missing.': 'Befehl fehlt.',
|
'Command is missing.': 'Befehl fehlt.',
|
||||||
'No open terminal': 'Kein offenes Terminal',
|
'No open terminal': 'Kein offenes Terminal',
|
||||||
'Upload files …': 'Dateien hochladen …',
|
'Upload files …': 'Dateien hochladen …',
|
||||||
|
'Root privileges are required. Log in as root or enter the password of a user with sudo rights.': 'Dafür sind root-Rechte nötig. Melde dich als root an oder gib das Passwort eines Benutzers mit sudo-Rechten ein.',
|
||||||
|
'sudo password': 'sudo-Passwort',
|
||||||
|
'sudo password for {user}': 'sudo-Passwort für {user}',
|
||||||
|
'Wrong password. sudo password for {user}': 'Falsches Passwort. sudo-Passwort für {user}',
|
||||||
// Support-Assistent
|
// Support-Assistent
|
||||||
'Delete chat': 'Chat löschen',
|
'Delete chat': 'Chat löschen',
|
||||||
'Delete chat?': 'Chat löschen?',
|
'Delete chat?': 'Chat löschen?',
|
||||||
@@ -579,7 +583,6 @@
|
|||||||
'Invalid value for {field}: {value}': 'Ungültiger Wert für {field}: {value}',
|
'Invalid value for {field}: {value}': 'Ungültiger Wert für {field}: {value}',
|
||||||
'Firewall session not found': 'Firewall-Sitzung nicht gefunden',
|
'Firewall session not found': 'Firewall-Sitzung nicht gefunden',
|
||||||
'Command failed with code {code}': 'Befehl fehlgeschlagen mit Code {code}',
|
'Command failed with code {code}': 'Befehl fehlgeschlagen mit Code {code}',
|
||||||
'Root privileges are required. Log in as root or save the password of a user with sudo rights on the host.': 'Dafür sind root-Rechte nötig. Melde dich als root an oder hinterlege am Host das Passwort eines Benutzers mit sudo-Rechten.',
|
|
||||||
'This host uses firewalld, which is not supported yet.': 'Dieser Host nutzt firewalld, das noch nicht unterstützt wird.',
|
'This host uses firewalld, which is not supported yet.': 'Dieser Host nutzt firewalld, das noch nicht unterstützt wird.',
|
||||||
'Neither UFW nor iptables was found on this host.': 'Auf diesem Host wurde weder UFW noch iptables gefunden.',
|
'Neither UFW nor iptables was found on this host.': 'Auf diesem Host wurde weder UFW noch iptables gefunden.',
|
||||||
'Port ranges and lists need a protocol (TCP or UDP).': 'Portbereiche und -listen brauchen ein Protokoll (TCP oder UDP).',
|
'Port ranges and lists need a protocol (TCP or UDP).': 'Portbereiche und -listen brauchen ein Protokoll (TCP oder UDP).',
|
||||||
|
|||||||
+13
-6
@@ -2,6 +2,7 @@
|
|||||||
// Kein Zugriff auf den Docker-Socket nötig: MrTerm führt `docker ps`, `docker start` … aus.
|
// Kein Zugriff auf den Docker-Socket nötig: MrTerm führt `docker ps`, `docker start` … aus.
|
||||||
// Fehlt die Berechtigung (Benutzer nicht in der Gruppe "docker"), wird sudo mit dem gespeicherten Host-Passwort versucht.
|
// Fehlt die Berechtigung (Benutzer nicht in der Gruppe "docker"), wird sudo mit dem gespeicherten Host-Passwort versucht.
|
||||||
const i18n = require('../i18n');
|
const i18n = require('../i18n');
|
||||||
|
const { acquireSudo } = require('./sudo');
|
||||||
|
|
||||||
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
||||||
const ACTIONS = { start: 'start', stop: 'stop', restart: 'restart', remove: 'rm -f' };
|
const ACTIONS = { start: 'start', stop: 'stop', restart: 'restart', remove: 'rm -f' };
|
||||||
@@ -37,12 +38,18 @@ class DockerManager {
|
|||||||
|
|
||||||
async open(id, host, onClose) {
|
async open(id, host, onClose) {
|
||||||
const { conn, jumps } = await this.ssh.connect(host, id);
|
const { conn, jumps } = await this.ssh.connect(host, id);
|
||||||
this.sessions.set(id, { conn, jumps, host, runtime: null, sudo: false });
|
this.sessions.set(id, { id, conn, jumps, host, runtime: null, sudo: false });
|
||||||
conn.on('close', () => { if (this.sessions.has(id)) { this.close(id); onClose(); } });
|
conn.on('close', () => { if (this.sessions.has(id)) { this.close(id); onClose(); } });
|
||||||
conn.on('error', () => {});
|
conn.on('error', () => {});
|
||||||
return this.list(id);
|
return this.list(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sudo-Passwort einmal pro Sitzung ermitteln (ohne Passwort, gespeichertes Host-Passwort oder Nachfrage)
|
||||||
|
async sudo(id, s) {
|
||||||
|
if (s.sudoPw === undefined) s.sudoPw = await acquireSudo(this.ssh, id, s.host, (c, stdin) => execOn(s.conn, c, stdin));
|
||||||
|
return s.sudoPw != null;
|
||||||
|
}
|
||||||
|
|
||||||
get(id) {
|
get(id) {
|
||||||
const s = this.sessions.get(id);
|
const s = this.sessions.get(id);
|
||||||
if (!s) throw new Error(i18n.t('Docker session not found'));
|
if (!s) throw new Error(i18n.t('Docker session not found'));
|
||||||
@@ -51,7 +58,7 @@ class DockerManager {
|
|||||||
|
|
||||||
run(s, args) {
|
run(s, args) {
|
||||||
const sudo = s.sudo ? "sudo -S -p '' " : '';
|
const sudo = s.sudo ? "sudo -S -p '' " : '';
|
||||||
return execOn(s.conn, `${sudo}${s.runtime} ${args}`, s.sudo ? `${s.host.password || ''}\n` : '');
|
return execOn(s.conn, `${sudo}${s.runtime} ${args}`, s.sudo ? `${s.sudoPw || ''}\n` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
async list(id) {
|
async list(id) {
|
||||||
@@ -62,7 +69,7 @@ class DockerManager {
|
|||||||
s.runtime = r.out.trim();
|
s.runtime = r.out.trim();
|
||||||
}
|
}
|
||||||
let r = await this.run(s, `ps -a --format ${LIST_FMT}`);
|
let r = await this.run(s, `ps -a --format ${LIST_FMT}`);
|
||||||
if (r.code && denied(r.err) && !s.sudo) {
|
if (r.code && denied(r.err) && !s.sudo && await this.sudo(id, s)) {
|
||||||
s.sudo = true;
|
s.sudo = true;
|
||||||
r = await this.run(s, `ps -a --format ${LIST_FMT}`);
|
r = await this.run(s, `ps -a --format ${LIST_FMT}`);
|
||||||
if (r.code) s.sudo = false;
|
if (r.code) s.sudo = false;
|
||||||
@@ -124,8 +131,8 @@ class DockerManager {
|
|||||||
// Shell-Skript ausführen, bei fehlender Berechtigung per sudo (nur mit gespeichertem Passwort)
|
// Shell-Skript ausführen, bei fehlender Berechtigung per sudo (nur mit gespeichertem Passwort)
|
||||||
async sh(s, script) {
|
async sh(s, script) {
|
||||||
let r = await execOn(s.conn, `sh -c ${q(script)}`);
|
let r = await execOn(s.conn, `sh -c ${q(script)}`);
|
||||||
if (r.code && /permission denied|operation not permitted/i.test(r.err) && s.host.password) {
|
if (r.code && /permission denied|operation not permitted/i.test(r.err) && await this.sudo(s.id, s)) {
|
||||||
r = await execOn(s.conn, `sudo -S -p '' sh -c ${q(script)}`, `${s.host.password}\n`);
|
r = await execOn(s.conn, `sudo -S -p '' sh -c ${q(script)}`, `${s.sudoPw}\n`);
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
@@ -158,7 +165,7 @@ class DockerManager {
|
|||||||
|
|
||||||
runCompose(s, compose, args) {
|
runCompose(s, compose, args) {
|
||||||
const sudo = s.sudo ? "sudo -S -p '' " : '';
|
const sudo = s.sudo ? "sudo -S -p '' " : '';
|
||||||
return execOn(s.conn, `${sudo}${compose} ${args}`, s.sudo ? `${s.host.password || ''}\n` : '');
|
return execOn(s.conn, `${sudo}${compose} ${args}`, s.sudo ? `${s.sudoPw || ''}\n` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
async readStack(id, file) {
|
async readStack(id, file) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Alle Werte aus der Oberfläche werden streng geprüft, bevor sie in einen Shell-Befehl gelangen.
|
// Alle Werte aus der Oberfläche werden streng geprüft, bevor sie in einen Shell-Befehl gelangen.
|
||||||
const i18n = require('../i18n');
|
const i18n = require('../i18n');
|
||||||
const { execOn } = require('./docker');
|
const { execOn } = require('./docker');
|
||||||
|
const { acquireSudo } = require('./sudo');
|
||||||
|
|
||||||
const lastLine = (s) => String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).slice(-1)[0] || '';
|
const lastLine = (s) => String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).slice(-1)[0] || '';
|
||||||
const RX = {
|
const RX = {
|
||||||
@@ -58,7 +59,7 @@ class FirewallManager {
|
|||||||
// Befehl mit root-Rechten ausführen
|
// Befehl mit root-Rechten ausführen
|
||||||
run(s, cmd) {
|
run(s, cmd) {
|
||||||
if (!s.sudo) return execOn(s.conn, `PATH=$PATH:/usr/sbin:/sbin ${cmd}`);
|
if (!s.sudo) return execOn(s.conn, `PATH=$PATH:/usr/sbin:/sbin ${cmd}`);
|
||||||
return execOn(s.conn, `sudo -S -p '' ${cmd}`, `${s.host.password || ''}\n`);
|
return execOn(s.conn, `sudo -S -p '' ${cmd}`, `${s.sudoPw || ''}\n`);
|
||||||
}
|
}
|
||||||
async runOk(s, cmd) {
|
async runOk(s, cmd) {
|
||||||
const r = await this.run(s, cmd);
|
const r = await this.run(s, cmd);
|
||||||
@@ -75,8 +76,8 @@ class FirewallManager {
|
|||||||
const uid = (await execOn(conn, 'id -u')).out.trim();
|
const uid = (await execOn(conn, 'id -u')).out.trim();
|
||||||
if (uid !== '0') {
|
if (uid !== '0') {
|
||||||
s.sudo = true;
|
s.sudo = true;
|
||||||
const r = await execOn(conn, "sudo -S -p '' -v", `${host.password || ''}\n`);
|
s.sudoPw = await acquireSudo(this.ssh, id, host, (c, stdin) => execOn(conn, c, stdin));
|
||||||
if (r.code) throw new Error(i18n.t('Root privileges are required. Log in as root or save the password of a user with sudo rights on the host.'));
|
if (s.sudoPw == null) throw new Error(i18n.t('Root privileges are required. Log in as root or enter the password of a user with sudo rights.'));
|
||||||
}
|
}
|
||||||
const t = await this.run(s, 'sh -c \'for c in ufw iptables ip6tables firewall-cmd; do command -v $c >/dev/null 2>&1 && echo $c; done; true\'');
|
const t = await this.run(s, 'sh -c \'for c in ufw iptables ip6tables firewall-cmd; do command -v $c >/dev/null 2>&1 && echo $c; done; true\'');
|
||||||
s.tools = t.out.split('\n').map((x) => x.trim()).filter(Boolean);
|
s.tools = t.out.split('\n').map((x) => x.trim()).filter(Boolean);
|
||||||
|
|||||||
+4
-3
@@ -4,6 +4,7 @@
|
|||||||
// nach 90 s selbst wieder her, falls MrTerm sich nicht erneut verbinden und die Änderung bestätigen kann.
|
// nach 90 s selbst wieder her, falls MrTerm sich nicht erneut verbinden und die Änderung bestätigen kann.
|
||||||
const i18n = require('../i18n');
|
const i18n = require('../i18n');
|
||||||
const { execOn } = require('./docker');
|
const { execOn } = require('./docker');
|
||||||
|
const { acquireSudo } = require('./sudo');
|
||||||
const nc = require('./netconf');
|
const nc = require('./netconf');
|
||||||
|
|
||||||
const STATE = '/var/lib/mrterm-net';
|
const STATE = '/var/lib/mrterm-net';
|
||||||
@@ -127,7 +128,7 @@ class NetworkConfigManager {
|
|||||||
root(s, script, conn = s.conn) {
|
root(s, script, conn = s.conn) {
|
||||||
const cmd = `sh -c "$(printf %s ${b64(script)} | base64 -d)"`;
|
const cmd = `sh -c "$(printf %s ${b64(script)} | base64 -d)"`;
|
||||||
if (!s.sudo) return execOn(conn, cmd);
|
if (!s.sudo) return execOn(conn, cmd);
|
||||||
return execOn(conn, `sudo -S -p '' ${cmd}`, `${s.host.password || ''}\n`);
|
return execOn(conn, `sudo -S -p '' ${cmd}`, `${s.sudoPw || ''}\n`);
|
||||||
}
|
}
|
||||||
async rootOk(s, script) {
|
async rootOk(s, script) {
|
||||||
const r = await this.root(s, script);
|
const r = await this.root(s, script);
|
||||||
@@ -142,8 +143,8 @@ class NetworkConfigManager {
|
|||||||
this.watch(id, s);
|
this.watch(id, s);
|
||||||
if ((await execOn(conn, 'id -u')).out.trim() !== '0') {
|
if ((await execOn(conn, 'id -u')).out.trim() !== '0') {
|
||||||
s.sudo = true;
|
s.sudo = true;
|
||||||
const r = await execOn(conn, "sudo -S -p '' -v", `${host.password || ''}\n`);
|
s.sudoPw = await acquireSudo(this.ssh, id, host, (c, stdin) => execOn(conn, c, stdin));
|
||||||
if (r.code) throw new Error(i18n.t('Root privileges are required. Log in as root or save the password of a user with sudo rights on the host.'));
|
if (s.sudoPw == null) throw new Error(i18n.t('Root privileges are required. Log in as root or enter the password of a user with sudo rights.'));
|
||||||
}
|
}
|
||||||
return this.read(id);
|
return this.read(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// sudo für Firewall, Netzwerk und Docker: erst ohne Passwort (NOPASSWD), dann mit dem am Host gespeicherten Passwort,
|
||||||
|
// sonst den Nutzer fragen (bis zu 3 Versuche). Das Passwort gilt nur für die offene Sitzung.
|
||||||
|
// run(cmd, stdin) → Promise<{ code, out, err }>
|
||||||
|
// → Passwort ('' bei NOPASSWD) oder null, wenn sudo nicht möglich ist bzw. abgebrochen wurde
|
||||||
|
const i18n = require('../i18n');
|
||||||
|
|
||||||
|
async function acquireSudo(ssh, sessionId, host, run) {
|
||||||
|
if ((await run('sudo -n true')).code === 0) return '';
|
||||||
|
const works = async (pw) => (await run("sudo -S -p '' -v", `${pw}\n`)).code === 0;
|
||||||
|
if (host.password && await works(host.password)) return host.password;
|
||||||
|
const user = `${host.username || ''}@${host.address}`.replace(/^@/, '');
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const pw = await ssh.askSecret(sessionId, {
|
||||||
|
title: i18n.t('sudo password'),
|
||||||
|
prompt: i ? i18n.t('Wrong password. sudo password for {user}', { user }) : i18n.t('sudo password for {user}', { user }),
|
||||||
|
echo: false,
|
||||||
|
host: host.label || host.address,
|
||||||
|
});
|
||||||
|
if (pw == null) return null;
|
||||||
|
if (await works(pw)) return pw;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { acquireSudo };
|
||||||
Reference in New Issue
Block a user