752f477a27
build / build (push) Successful in 1m16s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m9s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m51s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 26s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Bazy sa rdzeniem produktu i wlasnie zostaly kupione — a aplikacja nie miala ZADNEGO uwierzytelniania. Prezentacja to NodePort, wiec kazdy w LAN wchodzil bez logowania, a `/search` oddawal surowe wiersze do 50 000 na zapytanie. Bazy mogly wyjsc przez sama aplikacje, bez udzialu jakiegokolwiek LLM. - prezentacja: HTTP Basic (APP_USER/APP_PASSWORD) + limit zadan na IP (RATE_LIMIT_PER_MIN, domyslnie 120/min). Limit dziala TAKZE przed uwierzytelnieniem, zeby zgadywanie hasla i sondowanie API nie bylo darmowe. - logika i dane: token miedzywarstwowy X-Astrololo-Token (INTERNAL_TOKEN) — bez niego dalo sie ominac logowanie, uderzajac wprost w warstwe nizej. Warstwa danych oddaje surowe wiersze, wiec to najwrazliwszy punkt. - /search: gorny limit 50 000 -> 5000 (tyle realnie uzywa build_report). Publiczne /api/query zostaje na 200. - /health celowo publiczny (sondy k8s go nie uwierzytelnia). - swiadomie nie logujemy tresci zadan ani promptow — logi to kolejny nosnik. Fail-open przy braku konfiguracji (zgodnosc wstecz i dev), ale z GLOSNYM ostrzezeniem przy starcie, zeby nikt nie wdrozyl tego w przekonaniu, ze jest chroniony. Wlaczenie w produkcji wymaga ustawienia sekretow w repo deploy. Testy: 12 (prezentacja, nowy katalog + job w CI) i 5 (logika). Zweryfikowane na zywym stosie: bez hasla 401, z haslem 200, logika wprost bez tokenu 401, z tokenem 200, /health 200, limit 50000 odrzucony (422), a prezentacja nadal liczy horoskop przez logike. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
45 lines
1.4 KiB
Python
45 lines
1.4 KiB
Python
"""Klient HTTP do warstwy bazodanowej.
|
|
|
|
Jedyny punkt styku w dół. Gdyby warstwa bazodanowa zmieniła implementację
|
|
(Excel→SQL), tutaj nie zmienia się NIC — kontrakt /search jest stały.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from app.config import settings
|
|
|
|
|
|
def _auth_headers() -> dict[str, str]:
|
|
"""Token międzywarstwowy (LOG-32) — pusty, gdy ochrona wyłączona."""
|
|
token = os.getenv("INTERNAL_TOKEN", "")
|
|
return {"X-Astrololo-Token": token} if token else {}
|
|
|
|
|
|
class DataClient:
|
|
def __init__(self, base_url: str | None = None) -> None:
|
|
self.base_url = (base_url or settings.data_url).rstrip("/")
|
|
|
|
def search(
|
|
self,
|
|
key: str,
|
|
value: str,
|
|
exact: bool,
|
|
limit: int,
|
|
fields: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
payload = {"key": key, "value": value, "exact": exact, "limit": limit, "fields": fields}
|
|
with httpx.Client(timeout=max(settings.http_timeout, 30.0)) as client:
|
|
r = client.post(f"{self.base_url}/search", json=payload, headers=_auth_headers())
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def health(self) -> dict[str, Any]:
|
|
with httpx.Client(timeout=settings.http_timeout) as client:
|
|
r = client.get(f"{self.base_url}/health", headers=_auth_headers())
|
|
r.raise_for_status()
|
|
return r.json()
|