ftp idempotente e istanziabile più volte + logghing su stout x promtail
This commit is contained in:
162
src/utils/authorizers/database_authorizer.py
Normal file
162
src/utils/authorizers/database_authorizer.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Database-backed authorizer for FTP server that checks authentication against database in real-time.
|
||||
This ensures multiple FTP server instances stay synchronized without needing restarts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
|
||||
from pyftpdlib.authorizers import AuthenticationFailed, DummyAuthorizer
|
||||
|
||||
from utils.database.connection import connetti_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseAuthorizer(DummyAuthorizer):
|
||||
"""
|
||||
Custom authorizer that validates users against the database on every login.
|
||||
|
||||
This approach ensures that:
|
||||
- Multiple FTP server instances stay synchronized
|
||||
- User changes (add/remove/disable) are reflected immediately
|
||||
- No server restart is needed when users are modified
|
||||
"""
|
||||
|
||||
def __init__(self, cfg: dict) -> None:
|
||||
"""
|
||||
Initializes the authorizer with admin user only.
|
||||
Regular users are validated against database at login time.
|
||||
|
||||
Args:
|
||||
cfg: The configuration object.
|
||||
"""
|
||||
super().__init__()
|
||||
self.cfg = cfg
|
||||
|
||||
# Add admin user to in-memory authorizer (always available)
|
||||
self.add_user(
|
||||
cfg.adminuser[0], # username
|
||||
cfg.adminuser[1], # password hash
|
||||
cfg.adminuser[2], # home directory
|
||||
perm=cfg.adminuser[3] # permissions
|
||||
)
|
||||
|
||||
logger.info("DatabaseAuthorizer initialized with admin user")
|
||||
|
||||
def validate_authentication(self, username: str, password: str, handler: object) -> None:
|
||||
"""
|
||||
Validates user authentication against the database.
|
||||
|
||||
This method is called on every login attempt and checks:
|
||||
1. If user is admin, use in-memory credentials
|
||||
2. Otherwise, query database for user credentials
|
||||
3. Verify password hash matches
|
||||
4. Ensure user is not disabled
|
||||
|
||||
Args:
|
||||
username: The username attempting to login.
|
||||
password: The plain-text password provided.
|
||||
handler: The FTP handler object.
|
||||
|
||||
Raises:
|
||||
AuthenticationFailed: If authentication fails for any reason.
|
||||
"""
|
||||
# Hash the provided password
|
||||
password_hash = sha256(password.encode("UTF-8")).hexdigest()
|
||||
|
||||
# Check if user is admin (stored in memory)
|
||||
if username == self.cfg.adminuser[0]:
|
||||
if self.user_table[username]["pwd"] != password_hash:
|
||||
logger.warning(f"Failed admin login attempt for user: {username}")
|
||||
raise AuthenticationFailed("Invalid credentials")
|
||||
return
|
||||
|
||||
# For regular users, check database
|
||||
try:
|
||||
conn = connetti_db(self.cfg)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Query user from database
|
||||
cur.execute(
|
||||
f"SELECT ftpuser, hash, virtpath, perm, disabled_at FROM {self.cfg.dbname}.{self.cfg.dbusertable} WHERE ftpuser = %s",
|
||||
(username,)
|
||||
)
|
||||
|
||||
result = cur.fetchone()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if not result:
|
||||
logger.warning(f"Login attempt for non-existent user: {username}")
|
||||
raise AuthenticationFailed("Invalid credentials")
|
||||
|
||||
ftpuser, stored_hash, virtpath, perm, disabled_at = result
|
||||
|
||||
# Check if user is disabled
|
||||
if disabled_at is not None:
|
||||
logger.warning(f"Login attempt for disabled user: {username}")
|
||||
raise AuthenticationFailed("User account is disabled")
|
||||
|
||||
# Verify password
|
||||
if stored_hash != password_hash:
|
||||
logger.warning(f"Invalid password for user: {username}")
|
||||
raise AuthenticationFailed("Invalid credentials")
|
||||
|
||||
# Authentication successful - ensure user directory exists
|
||||
try:
|
||||
Path(virtpath).mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create directory for user {username}: {e}")
|
||||
raise AuthenticationFailed("System error")
|
||||
|
||||
# Temporarily add user to in-memory table for this session
|
||||
# This allows pyftpdlib to work correctly for the duration of the session
|
||||
if username not in self.user_table:
|
||||
self.add_user(ftpuser, stored_hash, virtpath, perm)
|
||||
|
||||
logger.info(f"Successful login for user: {username}")
|
||||
|
||||
except AuthenticationFailed:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Database error during authentication for user {username}: {e}", exc_info=True)
|
||||
raise AuthenticationFailed("System error")
|
||||
|
||||
def has_user(self, username: str) -> bool:
|
||||
"""
|
||||
Check if a user exists in the database or in-memory table.
|
||||
|
||||
This is called by pyftpdlib for various checks. We override it to check
|
||||
the database as well as the in-memory table.
|
||||
|
||||
Args:
|
||||
username: The username to check.
|
||||
|
||||
Returns:
|
||||
True if user exists and is enabled, False otherwise.
|
||||
"""
|
||||
# Check in-memory first (for admin and active sessions)
|
||||
if username in self.user_table:
|
||||
return True
|
||||
|
||||
# Check database for regular users
|
||||
try:
|
||||
conn = connetti_db(self.cfg)
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
f"SELECT COUNT(*) FROM {self.cfg.dbname}.{self.cfg.dbusertable} WHERE ftpuser = %s AND disabled_at IS NULL",
|
||||
(username,)
|
||||
)
|
||||
|
||||
count = cur.fetchone()[0]
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return count > 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database error checking user existence for {username}: {e}")
|
||||
return False
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
import os
|
||||
import signal
|
||||
from collections.abc import Callable, Coroutine
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any
|
||||
|
||||
import aiomysql
|
||||
@@ -33,24 +34,37 @@ class WorkerFormatter(logging.Formatter):
|
||||
|
||||
|
||||
def setup_logging(log_filename: str, log_level_str: str):
|
||||
"""Configura il logging globale.
|
||||
"""Configura il logging globale con rotation automatica.
|
||||
|
||||
Args:
|
||||
log_filename (str): Percorso del file di log.
|
||||
log_level_str (str): Livello di log (es. "INFO", "DEBUG").
|
||||
"""
|
||||
logger = logging.getLogger()
|
||||
handler = logging.FileHandler(log_filename)
|
||||
formatter = WorkerFormatter("%(asctime)s - PID: %(process)d.Worker-%(worker_id)s.%(name)s.%(funcName)s.%(levelname)s: %(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# Rimuovi eventuali handler esistenti e aggiungi il nostro
|
||||
# Rimuovi eventuali handler esistenti
|
||||
if logger.hasHandlers():
|
||||
logger.handlers.clear()
|
||||
logger.addHandler(handler)
|
||||
|
||||
# Handler per file con rotation (max 10MB per file, mantiene 5 backup)
|
||||
file_handler = RotatingFileHandler(
|
||||
log_filename,
|
||||
maxBytes=10 * 1024 * 1024, # 10 MB
|
||||
backupCount=5, # Mantiene 5 file di backup
|
||||
encoding="utf-8"
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Handler per console (utile per Docker)
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
log_level = getattr(logging, log_level_str.upper(), logging.INFO)
|
||||
logger.setLevel(log_level)
|
||||
logger.info("Logging configurato correttamente")
|
||||
logger.info("Logging configurato correttamente con rotation (10MB, 5 backup)")
|
||||
|
||||
|
||||
def setup_signal_handlers(logger: logging.Logger):
|
||||
|
||||
Reference in New Issue
Block a user