320a0ab24e
build-render / build (push) Failing after 8s
build-swisseph / build (push) Successful in 9s
build / build (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 5s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 4s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Failing after 4s
Ukrywanie jest teraz nadrzędne wobec wygody i czytelności komunikatów. Persona: konto z uprawnieniami files + files_input, zatrudnione wyłącznie do wgrywania plików. Nie ma się dowiedzieć, po co je wgrywa ani co program będzie robił — bo to rozgada. Audyt sześciu kanałów wycieku (statyki, HTML, sondowanie HTTP, ekran plików, odpowiedzi JSON i błędy, pozostałe warstwy) potwierdził 26 wycieków, każdy odtworzony uruchomionym kodem i zweryfikowany adwersarialnie. Ani jeden nie był przyciskiem. ZASÓB JEST CZĘŚCIĄ FUNKCJI /static/ omijało CAŁĄ bramkę (PUBLIC_PREFIXES), więc każdy skrypt i arkusz pobierał ktokolwiek, także niezalogowany, pod zgadywalnym adresem — a ich treść wymienia ekrany, dostawców modeli i przeznaczenie plików. Ruch ten nie trafiał przy tym ani do dziennika, ani pod limit żądań, więc wyciek był niewidoczny. Zasoby idą teraz trasą z bramką; każdy ma w features.STATIC uprawnienie swojego ekranu. Publiczny został jeden base.css, bo potrzebuje go ekran logowania. KOMENTARZ NIE JEDZIE NA DRUT Komentarze w CSS/JS opisywały funkcje pełnymi zdaniami po polsku — łącznie z „Wstrzymane widzi tylko administrator", czyli i mechanizmem kwarantanny, i istnieniem konta o wyższych uprawnieniach. _asset_body() usuwa je przy serwowaniu; w repozytorium zostają. styles.css rozbity na base.css + arkusz na ekran + x-ai.css. Jeden plik z wszystkimi selektorami był spisem treści programu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żaden ekran nie stracił reguły. base.html ładował skrypty kosmogramu na KAŻDEJ stronie — konto mające wyłącznie Pliki pobierało je przy wejściu na swój jedyny ekran, razem ze wzmianką o „przyszłej zakładce". Teraz dokłada je ekran, który ich używa. RÓŻNICA JEST INFORMACJĄ Komunikat po wgraniu pliku różnił się zależnie od wyniku walidacji — czyli był wyrocznią do odgadywania reguł, które ma znać tylko administrator — i mówił wprost, że plik „musi zatwierdzić administrator". Teraz jest jeden, ten sam. _logic_error wypisywał na ekran nazwę trasy, nazwę podsystemu, nazwę gałęzi rozwojowej i wewnętrzny host:port. Jedno zdanie dla wszystkich awarii, szczegóły do dziennika. Odsiew w jednym punkcie, nie w siedemnastu wywołaniach. Ponadto: stopka nie ogłasza architektury, /health nie nazywa warstwy, konto bez ekranów dostaje 404 zamiast tłumaczenia, ekran plików mówi o plikach zamiast o „bazach interpretacyjnych", klasy .house-warning i .account-card przemianowane na neutralne, a logic/data/render/engine-swisseph nie wystawiają już /docs ani /openapi.json i nie publikują portów na hoście. ZAPORA SŁOWNIKOWA test_slownik_zakazany.py nie sprawdza miejsc, tylko przechodzi wszystko, co dane konto może pobrać, i szuka słów, które nie mają prawa paść (87 pozycji dla tej persony). Nazwy funkcji, adresy ekranów i nazwy zasobów biorą się wprost z katalogu, więc nowa funkcja obejmuje się sama. Kontrola pozytywna pilnuje, żeby test nie przechodził dlatego, że program jest pusty. Sprawdzone: zapora puszczona na treść sprzed poprawek daje 16 trafień na samym styles.css i łapie każdy ze zneutralizowanych komunikatów. 358 testów zielonych, ekrany obejrzane w przeglądarce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
140 lines
4.9 KiB
Python
140 lines
4.9 KiB
Python
"""Warstwa BAZODANOWA — usługa HTTP.
|
|
|
|
Jedyne zadanie: przyjąć znormalizowane zapytanie z warstwy logicznej, wyszukać
|
|
dane (w Excelu z cache lub w SQL) i zwrócić je w górę. Nie zna warstwy logicznej
|
|
ani prezentacji.
|
|
"""
|
|
# build-marker: 2026-07-25 wymuszenie nowego obrazu po incydencie z tagiem :latest
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import binascii
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
|
|
from app import canary, files, link_crypto, security
|
|
from app.config import settings
|
|
from app.models import HealthInfo, SearchQuery, SearchResult
|
|
from pydantic import BaseModel
|
|
from app.providers.factory import build_provider
|
|
|
|
provider = build_provider(settings)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
provider.warmup() # zbuduj/odśwież indeks i cache przy starcie
|
|
yield
|
|
|
|
|
|
# Bez /docs, /redoc i /openapi.json: te trasy oddają KOMPLETNY katalog funkcji —
|
|
# nazwy tras, kształty żądań, listę dostawców modeli — i to każdemu, kto potrafi
|
|
# je otworzyć, bez żadnego logowania. Ochrona zakładek w prezentacji nic nie
|
|
# daje, gdy obok stoi usługa, która sama się spisuje (PRE-27).
|
|
app = FastAPI(title="astrololo · warstwa bazodanowa", lifespan=lifespan,
|
|
docs_url=None, redoc_url=None, openapi_url=None)
|
|
security.install(app, "danych") # token międzywarstwowy (LOG-32)
|
|
# Szyfrowanie łącza od logiki. PO `security.install`, żeby także odmowa
|
|
# tokenowa wracała zaszyfrowana — inaczej klient nie umiałby jej odczytać.
|
|
link_crypto.install(app, link_crypto.ENV_LOGIC_DATA, "danych")
|
|
|
|
|
|
@app.post("/search", response_model=SearchResult)
|
|
def search(query: SearchQuery) -> SearchResult:
|
|
result = provider.search(query)
|
|
# Rekordy-pułapki (DAN-26) odsiewamy TU, na wyjściu z warstwy danych — dzięki
|
|
# temu nie dotrą ani wyżej, ani do promptu LLM (LOG-30), niezależnie od dostawcy.
|
|
visible, report = canary.screen(result.rows, query.value)
|
|
if report["active"]:
|
|
result.rows = visible
|
|
result.total = len(visible)
|
|
return result
|
|
|
|
|
|
@app.get("/bases")
|
|
def bases() -> dict:
|
|
"""Bazy dostępne na udziale + metaopis i stan włączenia (DAN-15/PRE-09).
|
|
|
|
Same METADANE — nazwy plików, rozmiar, data. Żadnej treści baz, więc podgląd
|
|
listy nie jest kolejną drogą do ich wyniesienia."""
|
|
items = provider.list_bases()
|
|
return {"bases": items, "enabled": sum(1 for b in items if b["enabled"]), "total": len(items)}
|
|
|
|
|
|
# ── zarządzanie plikami baz (DAN-27) ─────────────────────────────────────
|
|
# Warstwa danych jest właścicielem plików, więc to ona nimi zarządza. Uprawnienia
|
|
# rozstrzyga PREZENTACJA (PRE-27) i przekazuje tu wynik jako `for_admin` / `by` —
|
|
# ta warstwa nie zna kont i nie ma jak ich znać. Nie jest to dziura: warstwa
|
|
# danych stoi za tokenem międzywarstwowym i szyfrowanym łączem, więc rozmawia
|
|
# z nią wyłącznie warstwa logiczna.
|
|
|
|
class FilesQuery(BaseModel):
|
|
for_admin: bool = False
|
|
|
|
|
|
class FileAction(BaseModel):
|
|
path: str
|
|
status: str = ""
|
|
by: str = ""
|
|
|
|
|
|
class FileUpload(BaseModel):
|
|
filename: str
|
|
content_b64: str
|
|
by: str = ""
|
|
|
|
|
|
class RulesUpdate(BaseModel):
|
|
rules: dict
|
|
|
|
|
|
@app.post("/files")
|
|
def files_list(q: FilesQuery) -> dict:
|
|
"""Rejestr plików. Kwarantanna WYCHODZI stąd tylko przy for_admin — filtrujemy
|
|
u źródła, żeby nie dało się jej odczytać z podglądu źródła strony."""
|
|
root = settings.excel_dir
|
|
return {"files": files.registry(root, for_admin=q.for_admin),
|
|
"rules": files.rules(root) if q.for_admin else {},
|
|
"root": str(root)}
|
|
|
|
|
|
@app.post("/files/status")
|
|
def files_status(a: FileAction) -> dict:
|
|
try:
|
|
row = files.set_status(settings.excel_dir, a.path, a.status, by=a.by)
|
|
except ValueError as e:
|
|
raise HTTPException(422, str(e)) from e
|
|
return {"path": a.path, "status": row.get("status")}
|
|
|
|
|
|
@app.post("/files/upload")
|
|
def files_upload(u: FileUpload) -> dict:
|
|
"""Plik wędruje w base64 wewnątrz zaszyfrowanego łącza — tym samym kanałem,
|
|
co reszta ruchu międzywarstwowego. Osobny, nieszyfrowany kanał na pliki
|
|
byłby obejściem PRE-16."""
|
|
try:
|
|
raw = base64.b64decode(u.content_b64, validate=True)
|
|
except (binascii.Error, ValueError) as e:
|
|
raise HTTPException(422, "Nieczytelna zawartość pliku.") from e
|
|
return files.store_upload(settings.excel_dir, u.filename, raw, by=u.by)
|
|
|
|
|
|
@app.post("/files/delete")
|
|
def files_delete(a: FileAction) -> dict:
|
|
try:
|
|
files.delete(settings.excel_dir, a.path)
|
|
except ValueError as e:
|
|
raise HTTPException(422, str(e)) from e
|
|
return {"deleted": a.path}
|
|
|
|
|
|
@app.post("/files/rules")
|
|
def files_rules(u: RulesUpdate) -> dict:
|
|
return {"rules": files.set_rules(settings.excel_dir, u.rules)}
|
|
|
|
|
|
@app.get("/health", response_model=HealthInfo)
|
|
def health() -> HealthInfo:
|
|
return provider.health()
|