v3 Phase C: Security-Härtung + UX-Feedback
Security: - Passwort-Leak dicht: os-update/reboot fuettern das sudo-Passwort ueber stdin (jobengine: neues stdin_data + sanitisierte Log-Zeile log_cmd). Verifiziert: Secret taucht nicht mehr im Job-Log/ps auf. - /api/status liefert "secured" -> Topbar-Security-Chip zeigt ehrlich, ob Token aktiv. - WS-Token-Auth funktioniert (Query-Param, war bereits in auth.py). - Bind bleibt 0.0.0.0 (kein Aussperren), kein Token erzwungen. UX-Feedback: - Schnellstart-Bug gefixt (Titel/Text brechen um, .qa-main column). - Cookbook: Sortier-Dropdown (Downloads/Likes/Aktualisiert/Trend) wie HuggingFace. - Server & Wartung: Buttons -> selbsterklaerende Aktionszeilen mit Beschreibung; Reboot nur mit rotem Icon (Vollrot-Bug behoben). - Live-Konsole: neue Option "System (ganzer Server)" -> journalctl ueber alle Units. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-4
@@ -17,18 +17,26 @@ JOBS: dict[str, dict] = {}
|
||||
_LOG_CAP = 400
|
||||
|
||||
|
||||
def _run_job(job_id: str, args: list[str], env: dict | None = None):
|
||||
def _run_job(job_id: str, args: list[str], env: dict | None = None, stdin_data: str | None = None):
|
||||
job = JOBS[job_id]
|
||||
job["state"] = "running"
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE if stdin_data is not None else None,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
env={**os.environ, **(env or {})},
|
||||
)
|
||||
if stdin_data is not None:
|
||||
# Secret (z.B. sudo-Passwort) ueber stdin fuettern — landet NICHT im Log/ps.
|
||||
try:
|
||||
proc.stdin.write(stdin_data) # type: ignore[union-attr]
|
||||
proc.stdin.close() # type: ignore[union-attr]
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
for line in proc.stdout: # type: ignore[union-attr]
|
||||
job["log"].append(line.rstrip("\n"))
|
||||
if len(job["log"]) > _LOG_CAP:
|
||||
@@ -43,16 +51,20 @@ def _run_job(job_id: str, args: list[str], env: dict | None = None):
|
||||
job["finished_at"] = time.time()
|
||||
|
||||
|
||||
def start_job(args: list[str], label: str, env: dict | None = None) -> str:
|
||||
def start_job(args: list[str], label: str, env: dict | None = None,
|
||||
stdin_data: str | None = None, log_cmd: str | None = None) -> str:
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
# log_cmd erlaubt eine sanitisierte Befehlszeile (kein Secret im Log), wenn stdin_data ein
|
||||
# Geheimnis (sudo-Passwort) traegt. Sonst die echten Argumente.
|
||||
first_line = log_cmd if log_cmd is not None else "$ " + " ".join(shlex.quote(a) for a in args)
|
||||
JOBS[job_id] = {
|
||||
"id": job_id,
|
||||
"label": label,
|
||||
"state": "queued",
|
||||
"log": [f"$ {' '.join(shlex.quote(a) for a in args)}"],
|
||||
"log": [first_line],
|
||||
"returncode": None,
|
||||
"started_at": time.time(),
|
||||
"finished_at": None,
|
||||
}
|
||||
threading.Thread(target=_run_job, args=(job_id, args, env), daemon=True).start()
|
||||
threading.Thread(target=_run_job, args=(job_id, args, env, stdin_data), daemon=True).start()
|
||||
return job_id
|
||||
|
||||
Reference in New Issue
Block a user