feat(astroklient): pule plików per konto i izolacja od produkcji (PRE-29)
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
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>
This commit is contained in:
@@ -39,12 +39,30 @@ _hits: dict[str, deque[float]] = {}
|
||||
MAX_TRACKED_CLIENTS = 4096
|
||||
|
||||
|
||||
def demo_user() -> str:
|
||||
return os.getenv("DEMO_USER", "demo")
|
||||
def accounts() -> dict[str, str]:
|
||||
"""Konta demo: `login:sekret`, po przecinku, ze zmiennej DEMO_USERS.
|
||||
|
||||
WIELE KONT, bo każde ma WŁASNĄ, niewidoczną dla pozostałych pulę baz (PRE-29).
|
||||
Wspólne konto oznaczałoby wspólną pulę, czyli klientów oglądających nawzajem
|
||||
swoje wgrania.
|
||||
|
||||
def demo_password() -> str:
|
||||
return os.getenv("DEMO_PASSWORD", "")
|
||||
Sekret najlepiej jako hash scrypt (`scrypt$sól$hash`) — ten sam format i ten
|
||||
sam skrypt make_user.py, co w głównej aplikacji.
|
||||
|
||||
Zgodność wstecz: pojedyncze DEMO_USER/DEMO_PASSWORD działa, gdy DEMO_USERS
|
||||
jest puste."""
|
||||
out: dict[str, str] = {}
|
||||
for entry in os.getenv("DEMO_USERS", "").split(","):
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
continue
|
||||
user, sep, secret = entry.partition(":")
|
||||
if sep and user.strip() and secret.strip():
|
||||
out[user.strip()] = secret.strip()
|
||||
if out:
|
||||
return out
|
||||
single = os.getenv("DEMO_PASSWORD", "")
|
||||
return {os.getenv("DEMO_USER", "demo"): single} if single else {}
|
||||
|
||||
|
||||
def rate_limit_per_min() -> int:
|
||||
@@ -56,7 +74,7 @@ def trust_proxy() -> bool:
|
||||
|
||||
|
||||
def auth_enabled() -> bool:
|
||||
return bool(demo_password())
|
||||
return bool(accounts())
|
||||
|
||||
|
||||
def _secret_matches(spec: str, password: str) -> bool:
|
||||
@@ -82,9 +100,12 @@ def authenticate(header: str | None) -> str | None:
|
||||
user, _, password = raw.partition(":")
|
||||
except (binascii.Error, UnicodeDecodeError, IndexError):
|
||||
return None
|
||||
if user != demo_user():
|
||||
spec = accounts().get(user)
|
||||
if spec is None:
|
||||
return None
|
||||
return user if _secret_matches(demo_password(), password) else None
|
||||
# Nieznany login i złe hasło traktujemy tak samo — odmowa nie mówi, które
|
||||
# konto istnieje, a przy demo rozdawanym szeroko to realna informacja.
|
||||
return user if _secret_matches(spec, password) else None
|
||||
|
||||
|
||||
def client_ip(request: Request) -> str:
|
||||
@@ -142,9 +163,9 @@ def install(app) -> None:
|
||||
_setup_audit_logging()
|
||||
if not auth_enabled():
|
||||
log.warning(
|
||||
"UWAGA: DEMO_PASSWORD nie ustawione — astroklient stoi OTWARTY, a pracuje "
|
||||
"na tej samej warstwie danych co produkcja, więc wystawia treść "
|
||||
"oryginalnych baz interpretacyjnych każdemu, kto zna adres."
|
||||
"UWAGA: brak kont (DEMO_USERS / DEMO_PASSWORD) — astroklient stoi OTWARTY. "
|
||||
"Bez logowania nie ma też PULI: wszyscy pracowaliby na jednym, wspólnym "
|
||||
"zbiorze baz, widząc nawzajem swoje wgrania."
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
|
||||
Reference in New Issue
Block a user