from pathlib import Path import typer from tht.adapters.factory import build_dwh from tht.cli.config_cmd import CONFIG_OPT from tht.config import ConfigError, load_config from tht.db.fetch_ca import CaFetchError, describe_pem, fetch_chain_pem, parse_host_port db_app = typer.Typer(help="Operazioni sul database target") @db_app.command("ping") def ping_cmd(config: Path = CONFIG_OPT) -> None: """Testa la connessione e verifica che l'utente sia effettivamente read-only.""" try: cfg = load_config(config) except ConfigError as e: typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True) raise typer.Exit(code=1) health = build_dwh(cfg).health() if not health.ok: message = ( f"ERRORE: DWH non accessibile via REST (risposta: {health.detail})." if health.error_kind == "inaccessible" else f"ERRORE di connessione: {health.detail}" ) typer.secho(message, fg=typer.colors.RED, err=True) raise typer.Exit(code=1) if health.endpoint: typer.secho(f"OK: connesso via REST a {health.endpoint} (schema {health.schema})", fg=typer.colors.GREEN) typer.secho("OK: accesso read-only garantito dall'API (solo SELECT/WITH).", fg=typer.colors.GREEN) return typer.secho(f"OK: connesso a {health.database} (schema {health.schema})", fg=typer.colors.GREEN) if not health.read_only: typer.secho( f"ERRORE: l'utente '{cfg.database.user}' NON e' read-only.", fg=typer.colors.RED, err=True ) if health.writable_tables: typer.echo(f" Tabelle scrivibili: {', '.join(health.writable_tables[:10])}", err=True) if health.can_create: typer.echo(f" L'utente puo' creare oggetti nello schema {health.schema}.", err=True) typer.echo(" Crea un ruolo read-only con scripts/create_readonly_role.sql.", err=True) raise typer.Exit(code=2) typer.secho("OK: l'utente e' read-only sullo schema target.", fg=typer.colors.GREEN) @db_app.command("fetch-ca") def fetch_ca_cmd( config: Path = CONFIG_OPT, out: Path = typer.Option( Path("config/ca-chain.pem"), "--out", "-o", help="File PEM di destinazione." ), ) -> None: """Scarica la catena CA presentata dal server REST e la salva in un bundle PEM. Utile sulle postazioni *workstation* dietro una CA interna: il file va poi puntato con `THT_SSL_CA` nel `.env` (lo consuma `requests`). NON tocca il trust store dell'OS. """ try: cfg = load_config(config) except ConfigError as e: typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True) raise typer.Exit(code=1) if cfg.rest is None: typer.secho( "ERRORE: comando pensato per postazioni con accesso REST. " "Configura la sezione `rest` (base_url) nel workspace yaml.", fg=typer.colors.RED, err=True, ) raise typer.Exit(code=1) # Host del DWH REST; se il vector REST รจ su host diverso, aggiungi anche quello. targets: list[tuple[str, int]] = [parse_host_port(cfg.rest.base_url)] if cfg.vector_rest is not None: vec = parse_host_port(cfg.vector_rest.base_url) if vec not in targets: targets.append(vec) pems: list[str] = [] seen: set[str] = set() try: for host, port in targets: typer.echo(f"Recupero catena TLS da {host}:{port} ...") for pem in fetch_chain_pem(host, port, cfg.rest.timeout): key = pem.strip() if key not in seen: seen.add(key) pems.append(pem if pem.endswith("\n") else pem + "\n") except CaFetchError as e: typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True) raise typer.Exit(code=1) out.parent.mkdir(parents=True, exist_ok=True) out.write_text("".join(pems)) abs_out = out.resolve() typer.secho( f"OK: salvati {len(pems)} certificati -> {abs_out}", fg=typer.colors.GREEN ) typer.echo("Controlla che corrispondano alla CA interna attesa:") for i, pem in enumerate(pems, 1): desc = describe_pem(pem) if desc: typer.echo(f" [{i}] {desc}") typer.echo( "NOTA: il recupero non verifica la fiducia; la verifica avviene al prossimo " "`tht db ping`, quando THT_SSL_CA punta a questo bundle." ) # Controlla se la config *effettiva* (THT_SSL_CA nel .env + ssl_ca nel workspace yaml) # gia' punta al bundle appena salvato: in caso contrario, il certificato non e' # ancora collegato e il `db ping` fallirebbe ancora con certificate verify failed. endpoints = [("rest", cfg.rest)] if cfg.vector_rest is not None: endpoints.append(("vector_rest", cfg.vector_rest)) not_wired = [name for name, ep in endpoints if not _points_to(ep.ssl_ca, abs_out)] if not not_wired: typer.secho( "OK: THT_SSL_CA punta gia' a questo bundle (rest" + (", vector_rest" if cfg.vector_rest is not None else "") + "). Lancia `tht db ping`.", fg=typer.colors.GREEN, ) return typer.secho( "\nATTENZIONE: il certificato non e' ancora collegato " f"(ssl_ca di {', '.join(not_wired)} non punta a questo bundle). Per attivarlo:", fg=typer.colors.YELLOW, ) typer.echo(f" 1. nel .env imposta: THT_SSL_CA={abs_out}") typer.echo( " 2. in il workspace yaml, sotto `rest:`" + (" e `vector_rest:`" if cfg.vector_rest is not None else "") + ", decommenta/aggiungi:" ) typer.echo(" ssl_ca: ${THT_SSL_CA}") typer.echo("Poi rilancia `tht db ping`.") def _points_to(ssl_ca: str | None, target: Path) -> bool: """True se `ssl_ca` (path risolto) coincide col bundle appena salvato.""" if not ssl_ca: return False try: return Path(ssl_ca).resolve() == target except (OSError, ValueError): return False