From a7a53f064cb2d728afd4e9a1b8a2416cbd95cc3f Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:08:37 +0200 Subject: [PATCH] fix --- app/config.py | 33 ++++++++++++++++++++++++++++++--- app/routes/apps.py | 33 +++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app/config.py b/app/config.py index 4569a96..ea8a0c3 100644 --- a/app/config.py +++ b/app/config.py @@ -1,9 +1,36 @@ DEPLOY_SCRIPT = "/tools/deploy-app.sh" + +# Holé názvy shell skriptů, které smí Portál zařadit jako Worker run_script job. +# Worker přijímá v poli script_name jen HOLÝ název bez cesty (adresář scripts/ si doplní sám) +# a cokoli s cestou ("scripts/...", "../...", absolutní cesta) odmítne hláškou +# "Invalid script name". Stejné pravidlo má i portál sám v +# app/routes/scheduled_scripts.validate_script_name. Tuto množinu validujeme i tady, ať +# nezaložíme job, který Worker stejně zahodí (přesně na tom padal job 221). +ALLOWED_WORKER_SCRIPTS = { + "deploy-app.sh", + "delete-app.sh", + "deploy-core-service.sh", + "generate-caddyfile.sh", + "redeploy-enabled-apps.sh", + "backup-appfactory.sh", +} + # Mazání aplikace NEPROVÁDÍ portál přímo – běží bez práv k workspace, takže rm padá na # "Permission denied". Portál jen zařadí run_script job a Worker spustí tento skript -# v appfactory-tools s právy serveru. Prefix "scripts/" je povinný, jinak ho worker hledá -# ve špatné složce (stejná konvence jako app/routes/runtime.py). -DELETE_APP_SCRIPT_NAME = "scripts/delete-app.sh" +# v appfactory-tools/scripts/ s právy serveru. +# POZOR: holý název, NE "scripts/delete-app.sh" – cesta v názvu = "Invalid script name". +DELETE_APP_SCRIPT_NAME = "delete-app.sh" + + +def is_allowed_worker_script(script_name: str) -> bool: + """True jen pro holý, povolený název skriptu pro Worker run_script job. + Odmítne prázdný název, název nekončící .sh, název s cestou ("/", "\\", "..") i název + mimo allowlist – tedy přesně případy, na které Worker hlásí "Invalid script name".""" + if not script_name or not script_name.endswith(".sh"): + return False + if "/" in script_name or "\\" in script_name or ".." in script_name: + return False + return script_name in ALLOWED_WORKER_SCRIPTS BACKUP_SCRIPT = "/tools/backup-appfactory.sh" RESTORE_SCRIPT = "/tools/restore-appfactory.sh" GENERATE_COMPOSE_SCRIPT = "/tools/generate-apps-compose.sh" diff --git a/app/routes/apps.py b/app/routes/apps.py index db968e8..fd23872 100644 --- a/app/routes/apps.py +++ b/app/routes/apps.py @@ -13,6 +13,7 @@ from ..config import ( GENERATE_COMPOSE_SCRIPT, get_appfactory_host, get_gitea_public_url, + is_allowed_worker_script, read_env_value, ) from ..db.apps import ( @@ -1520,10 +1521,34 @@ def create_app( @router.post("/delete-app", response_class=HTMLResponse) def delete_app(app_id: str = Form(...), user=Depends(require_admin)): - # Portál NEMAŽE workspace přímo – nemá k němu práva (rm padá na "Permission denied"). - # Místo toho zařadí run_script job; mazání (container, image, gitea repo, workspace) - # provede Worker přes scripts/delete-app.sh s právy serveru. Veškerá runtime/Docker - # logika patří do Workeru a shell skriptů, ne do portálu. + # Portál NEMAŽE workspace/container/image/gitea repo přímo – běží bez práv k workspace, + # takže rm padá na "Permission denied". Místo toho jen zařadí run_script job a mazání + # provede Worker přes delete-app.sh s právy serveru. Veškerá runtime/Docker logika patří + # do Workeru a shell skriptů, ne do portálu. + # + # KONTRAKT NÁZVU AKCE (proč dříve padalo "Invalid script name"): + # - do jobu se ukládá HOLÝ název skriptu: script_name = "delete-app.sh" + # (stejný formát jako ostatní run_script joby, viz preflight-check.sh / bootstrap-v2.sh + # a portálová validace v scheduled_scripts.validate_script_name); + # - Worker přijímá jen holý název, adresář scripts/ si doplní sám, a cokoli s cestou + # ("scripts/delete-app.sh", "../...", absolutní cesta) odmítne jako "Invalid script name". + # Job 221 padal právě proto, že se posílalo "scripts/delete-app.sh" (název s cestou). + # Stejný allowlist/validaci děláme i tady, ať portál nikdy nezaloží job, který Worker zahodí. + if not is_allowed_worker_script(DELETE_APP_SCRIPT_NAME): + return render_result( + title="Smazání služby: CHYBA", + back_url="/portal/apps", + sections=[ + ( + "Chyba", + f"Neplatný název skriptu pro Worker job: '{DELETE_APP_SCRIPT_NAME}'. " + "Musí to být holý povolený název bez cesty (např. 'delete-app.sh'), jinak " + "Worker job odmítne hláškou 'Invalid script name'. Job nebyl vytvořen.", + ), + ], + user=user, + ) + payload = { "script_name": DELETE_APP_SCRIPT_NAME, "args": [app_id],