148 lines
5.8 KiB
Python
148 lines
5.8 KiB
Python
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:
|
|
typer.secho(f"ERRORE di connessione: {health.detail}", 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
|