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/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" CATALOG_FILE = "/opt/appfactory/apps/catalog.yml" APPFACTORY_ENV = "/opt/appfactory/config/appfactory.env" DEFAULT_BACKUP_DIR = "/opt/appfactory/backups" DEFAULT_GITEA_ORG = "appfactory" INTERNAL_GITEA_URL = "http://appfactory-gitea:3000" PORTAL_PREFIX = "/portal" AUTH_MODES = {"google", "mixed", "local"} def read_env_value(key: str, default: str = "") -> str: try: with open(APPFACTORY_ENV, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line.startswith(f"{key}="): return line.split("=", 1)[1].strip().strip('"') except FileNotFoundError: # appfactory.env nemusí existovat (např. lokální vývoj) – očekávaný stav, vrátíme default. pass except Exception as exc: # Neočekávaná chyba čtení configu – nesmí být tichá. from app.logging_config import get_logger get_logger(__name__).warning("Čtení %s (klíč %s) selhalo: %s", APPFACTORY_ENV, key, exc) return default def read_env_bool(key: str, default: bool = False) -> bool: value = read_env_value(key, "") if value == "": return default return value.strip().lower() in {"1", "true", "yes", "on"} def get_portal_public_url() -> str: public_url = read_env_value("APPFACTORY_PORTAL_PUBLIC_URL", "").rstrip("/") if public_url: return public_url domain = read_env_value("APPFACTORY_PORTAL_DOMAIN", "").strip() if read_env_bool("APPFACTORY_ENABLE_HTTPS") and domain: return f"https://{domain}" return "" def get_google_redirect_uri() -> str: redirect_uri = read_env_value("GOOGLE_REDIRECT_URI", "").strip() if redirect_uri: return redirect_uri public_url = get_portal_public_url() if public_url: return f"{public_url}{PORTAL_PREFIX}/auth/google/callback" return "" def get_gitea_public_url() -> str: return (read_env_value("GITEA_URL", "") or read_env_value("GITEA_ROOT_URL", "")).rstrip("/") def get_gitea_server_url() -> str: return ( read_env_value("GITEA_SERVER_URL", "") or read_env_value("GITEA_INTERNAL_URL", "") or get_gitea_public_url() or INTERNAL_GITEA_URL ).rstrip("/") def get_gitea_admin_token() -> str: return ( read_env_value("GITEA_ADMIN_TOKEN", "") or read_env_value("GITEA_API_TOKEN", "") or read_env_value("GITEA_TOKEN", "") ).strip() def get_gitea_redirect_uri() -> str: redirect_uri = read_env_value("GITEA_OAUTH_REDIRECT_URI", "").strip() if redirect_uri: return redirect_uri public_url = get_portal_public_url() if public_url: return f"{public_url}{PORTAL_PREFIX}/auth/gitea/callback" return "" def is_gitea_oauth_button_enabled() -> bool: return bool( get_gitea_public_url() and read_env_value("GITEA_OAUTH_CLIENT_ID", "") and read_env_value("GITEA_OAUTH_CLIENT_SECRET", "") and get_gitea_redirect_uri() ) def get_appfactory_host(request_host: str = "") -> str: configured_host = read_env_value("APPFACTORY_HOST", "").strip() if configured_host: return configured_host public_url = get_portal_public_url() if public_url: return public_url.split("://", 1)[-1].split("/", 1)[0].split(":", 1)[0] return (request_host or "").split(":", 1)[0] def get_auth_domain_readiness() -> dict[str, bool]: google_enabled = read_env_bool("GOOGLE_OAUTH_ENABLED") google_client_id = read_env_value("GOOGLE_CLIENT_ID", "") google_client_secret = read_env_value("GOOGLE_CLIENT_SECRET", "") google_redirect_uri = get_google_redirect_uri() return { "portal_public_url_configured": bool(get_portal_public_url()), "auth_mode_google": get_auth_mode() == "google", "auth_mode_mixed": get_auth_mode() == "mixed", "auth_mode_local": get_auth_mode() == "local", "google_oauth_enabled": google_enabled, "google_oauth_configured": bool(google_client_id and google_client_secret and google_redirect_uri), "google_redirect_uri_configured": bool(google_redirect_uri), "https_enabled": read_env_bool("APPFACTORY_ENABLE_HTTPS"), } def get_auth_mode() -> str: mode = read_env_value("AUTH_MODE", "mixed").strip().lower() if mode in AUTH_MODES: return mode return "mixed" def is_google_oauth_button_enabled() -> bool: return bool( read_env_bool("GOOGLE_OAUTH_ENABLED") and read_env_value("GOOGLE_CLIENT_ID", "") and read_env_value("GOOGLE_CLIENT_SECRET", "") and get_google_redirect_uri() )