From 84a171ca73f5b364e2edc665acaedd55a3008a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Tue, 13 Oct 2026 20:41:24 -0600 Subject: [PATCH] =?UTF-8?q?htpasswd:=20reescritura=20en=20sitio=20cuando?= =?UTF-8?q?=20/etc/nginx=20no=20es=20escribible=20(alta=20de=20usuarios=20?= =?UTF-8?q?y=20cambio=20de=20contrase=C3=B1a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/backend/app.py b/backend/app.py index b03c665..96b02ec 100644 --- a/backend/app.py +++ b/backend/app.py @@ -495,18 +495,33 @@ def _leer_htpasswd(): def _escribir_htpasswd(lineas): - """Reescribe el archivo de forma atómica, conservando permisos y dueño.""" + """Reescribe el archivo de usuarios. + + Si la carpeta es escribible (p. ej. en pruebas) se hace de forma atómica (temporal + rename) + conservando permisos y dueño. En producción el archivo vive en /etc/nginx, que es de root y + NO es escribible por el servicio aunque el archivo sí lo sea (chown leo:www-data + 640): + entonces se reescribe EN SITIO (r+ / truncate), que solo requiere permiso sobre el archivo. + Las rutas que llaman a esta función toman antes el candado HTPASSWD_LOCK.""" + contenido = "\n".join(l for l in lineas if l is not None) + "\n" carpeta = os.path.dirname(HTPASSWD) or "." st = os.stat(HTPASSWD) - tmp = os.path.join(carpeta, ".htpasswd.tmp-" + uuid.uuid4().hex[:8]) - with open(tmp, "w", encoding="utf-8") as f: - f.write("\n".join(lineas) + "\n") - os.chmod(tmp, st.st_mode & 0o7777) - try: - os.chown(tmp, st.st_uid, st.st_gid) - except PermissionError: - pass - os.replace(tmp, HTPASSWD) + if os.access(carpeta, os.W_OK): + tmp = os.path.join(carpeta, ".htpasswd.tmp-" + uuid.uuid4().hex[:8]) + with open(tmp, "w", encoding="utf-8") as f: + f.write(contenido) + os.chmod(tmp, st.st_mode & 0o7777) + try: + os.chown(tmp, st.st_uid, st.st_gid) + except PermissionError: + pass + os.replace(tmp, HTPASSWD) + return + with open(HTPASSWD, "r+", encoding="utf-8") as f: # PermissionError aquí = archivo no escribible + f.seek(0) + f.write(contenido) + f.truncate() + f.flush() + os.fsync(f.fileno()) @app.get("/api/usuarios")