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>
51 lines
1.6 KiB
Python
51 lines
1.6 KiB
Python
"""Uwierzytelnianie międzywarstwowe (LOG-32).
|
|
|
|
Warstwa logiczna oddaje treść baz interpretacyjnych, więc samo zalogowanie w
|
|
prezentacji nie wystarczy — bez tego kontrolera wystarczyłoby uderzyć w logikę
|
|
z pominięciem UI. Gdy ustawiono INTERNAL_TOKEN, każde żądanie (poza /health)
|
|
musi go przynieść w nagłówku X-Astrololo-Token.
|
|
|
|
Bez INTERNAL_TOKEN kontrola jest wyłączona (dev / zgodność wstecz) — wtedy przy
|
|
starcie leci ostrzeżenie.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import secrets
|
|
|
|
from fastapi import Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
log = logging.getLogger("astrololo.security")
|
|
|
|
HEADER = "X-Astrololo-Token"
|
|
PUBLIC_PATHS = frozenset({"/health"})
|
|
|
|
|
|
def token() -> str:
|
|
"""Czytany leniwie — konfiguracja może się zmienić bez importu modułu."""
|
|
return os.getenv("INTERNAL_TOKEN", "")
|
|
|
|
|
|
def enabled() -> bool:
|
|
return bool(token())
|
|
|
|
|
|
def install(app, layer: str) -> None:
|
|
if not enabled():
|
|
log.warning(
|
|
"UWAGA: INTERNAL_TOKEN nie ustawiony — warstwa %s przyjmuje żądania od "
|
|
"kogokolwiek, kto ma do niej dostęp sieciowy.", layer,
|
|
)
|
|
|
|
@app.middleware("http")
|
|
async def _guard(request: Request, call_next):
|
|
if request.url.path in PUBLIC_PATHS or not enabled():
|
|
return await call_next(request)
|
|
got = request.headers.get(HEADER, "")
|
|
if not secrets.compare_digest(got, token()):
|
|
return JSONResponse({"detail": "Brak lub błędny token międzywarstwowy."},
|
|
status_code=401)
|
|
return await call_next(request)
|