2d7775f9b3
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m17s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astroklienta (wersja demo) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m25s
Testy / Testy astroklienta (wersja demo) (pull_request) Successful in 9m26s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 5s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 4s
Demo ma być rozdawane szeroko i różnym osobom, więc pierwsza wersja — jedno konto na produkcyjnej warstwie danych — nie nadawała się do użycia: każdy dostawałby dostęp do oryginalnych baz, a wgrania jednego klienta widzieliby wszyscy. IZOLACJA OD PRODUKCJI. Warstwa danych i logiczna demo są osobne (manifesty w repo deploy). Osobna musi być TEŻ LOGICZNA, bo zna ona jeden adres warstwy danych — demo korzystające z produkcyjnej logiki i tak trafiłoby na produkcyjne bazy. PULE PER KONTO w warstwie danych. Zapytanie i lista plików niosą nazwę puli; puste = cały udział, czyli produkcja działa dokładnie jak dotąd i o pulach nic nie wie. Nazwa puli przechodzi przez sito dopuszczające wyłącznie znaki bezpieczne w nazwie katalogu — „../..” albo ukośnik wyprowadziłyby zapytanie wprost do cudzych baz, więc sito ZAMIENIA podejrzane znaki zamiast ufać, że nikt ich nie poda. PULA MUSI BYĆ W KLUCZU CACHE ZAPYTAŃ. Bez tego wynik policzony dla jednego konta trafiłby z cache do drugiego — cicha wymiana treści baz między klientami, niewidoczna w logach i nie do wykrycia z zewnątrz. Osobny test tego pilnuje. PULA WYNIKA Z LOGINU, nigdy z żądania. Klient warstwy logicznej jest budowany per żądanie i związany z pulą zalogowanej osoby; gdyby nazwa przychodziła z formularza, wystarczyłoby podstawić cudzy login. Test wysyła `tenant`, `user` i `login` w polach formularza i sprawdza, że nie mają na nią wpływu. Pulę wstrzykujemy w INSTANCJĘ klienta, nie w sygnatury metod. Argumentem trzeba by ją przeprowadzić przez protokół DataSource i build_report — kod, który o kontach nie ma prawa nic wiedzieć — a każde nowe wywołanie byłoby okazją, żeby o nią zapomnieć i sięgnąć nie tam. Konta demo to lista `login:sekret` (DEMO_USERS), bo jedno wspólne konto oznaczałoby wspólną pulę. Format i skrypt haseł te same, co w głównej aplikacji. Pula klienta to JEDEN KATALOG, więc przejście na pełną wersję nie oznacza utraty wgrań — procedurę importu opisuje runbook w repo deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
4.4 KiB
Python
99 lines
4.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 import link_crypto
|
|
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 {}
|
|
|
|
|
|
def _link() -> link_crypto.Link | None:
|
|
"""Klucz łącza logika↔dane. Czytany przy każdym wywołaniu, bo konfiguracja
|
|
może się zmienić bez restartu procesu (testy, podmiana sekretu)."""
|
|
key = link_crypto.key_from_env(link_crypto.ENV_LOGIC_DATA)
|
|
return link_crypto.Link(key) if key else None
|
|
|
|
|
|
class DataClient:
|
|
"""Rozmówca warstwy danych, opcjonalnie ZWIĄZANY Z PULĄ KONTA (PRE-29).
|
|
|
|
Pula siedzi w INSTANCJI, nie w sygnaturach metod. Gdyby jechała argumentem,
|
|
trzeba by ją przeprowadzić przez protokół DataSource i przez build_report —
|
|
czyli przez kod, który o kontach nie ma prawa nic wiedzieć. A przy okazji
|
|
każde nowe wywołanie byłoby okazją, żeby o nią zapomnieć i sięgnąć nie tam.
|
|
|
|
Puste = cały udział, czyli zachowanie produkcji."""
|
|
|
|
def __init__(self, base_url: str | None = None, tenant: str = "") -> None:
|
|
self.base_url = (base_url or settings.data_url).rstrip("/")
|
|
self.tenant = tenant or ""
|
|
|
|
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, "tenant": self.tenant}
|
|
with httpx.Client(timeout=max(settings.http_timeout, 30.0)) as client:
|
|
return link_crypto.call_json(client, "POST", f"{self.base_url}/search",
|
|
payload=payload, headers=_auth_headers(),
|
|
link=_link())
|
|
|
|
def bases(self) -> dict[str, Any]:
|
|
"""Lista baz na udziale + metaopis (DAN-15) — same metadane, bez treści."""
|
|
with httpx.Client(timeout=settings.http_timeout) as client:
|
|
return link_crypto.call_json(client, "GET", f"{self.base_url}/bases",
|
|
headers=_auth_headers(), link=_link())
|
|
|
|
|
|
# ── zarządzanie plikami baz (DAN-27) ────────────────────────────────
|
|
# Jedna metoda na trasę, bez sprytnego generyka: te wywołania różnią się
|
|
# skutkiem (odczyt / zapis / skasowanie), a ujednolicenie ich w jedno
|
|
# `call(path, payload)` zaciera tę różnicę dokładnie tam, gdzie jest ważna.
|
|
|
|
def files_list(self, for_admin: bool = False) -> dict[str, Any]:
|
|
return self._files_post("/files", {"for_admin": for_admin, "tenant": self.tenant})
|
|
|
|
def files_status(self, path: str, status: str, by: str = "") -> dict[str, Any]:
|
|
return self._files_post("/files/status", {"path": path, "status": status, "by": by, "tenant": self.tenant})
|
|
|
|
def files_upload(self, filename: str, content_b64: str, by: str = "") -> dict[str, Any]:
|
|
return self._files_post("/files/upload",
|
|
{"filename": filename, "content_b64": content_b64, "by": by, "tenant": self.tenant})
|
|
|
|
def files_delete(self, path: str) -> dict[str, Any]:
|
|
return self._files_post("/files/delete", {"path": path, "tenant": self.tenant})
|
|
|
|
def files_rules(self, rules: dict) -> dict[str, Any]:
|
|
return self._files_post("/files/rules", {"rules": rules, "tenant": self.tenant})
|
|
|
|
def _files_post(self, path: str, payload: dict) -> dict[str, Any]:
|
|
with httpx.Client(timeout=settings.http_timeout) as client:
|
|
return link_crypto.call_json(client, "POST", f"{self.base_url}{path}",
|
|
payload=payload, headers=_auth_headers(), link=_link())
|
|
|
|
def health(self) -> dict[str, Any]:
|
|
# /health celowo poza szyfrowaniem — pukają tu sondy k8s, które klucza
|
|
# nie mają, a nie przechodzi tędy nic z baz.
|
|
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()
|