feat(astroklient): pule plików per konto i izolacja od produkcji (PRE-29)

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:
2026-08-17 19:00:39 +02:00
parent e7b6d5013b
commit caf4fd80d1
12 changed files with 301 additions and 79 deletions
+16
View File
@@ -36,6 +36,22 @@ from datetime import datetime, timezone
from pathlib import Path
ACTIVE, READY, ARCHIVED, QUARANTINE = "active", "ready", "archived", "quarantine"
# Nazwa puli może przyjść z zewnątrz (login konta w wersji demo), więc przechodzi
# przez sito: wyłącznie znaki bezpieczne w nazwie katalogu. Bez tego „../..” albo
# ukośnik wyprowadziłyby zapytanie poza udział — czyli wprost do cudzych baz.
_TENANT_OK = re.compile(r"[^A-Za-z0-9._-]")
def tenant_root(root: Path | str, tenant: str = "") -> Path:
"""Katalog, w którym mieszka pula danego konta.
Puste `tenant` = cały udział, czyli zachowanie produkcji. Nazwa jest
OCZYSZCZANA, a nie sprawdzana i odrzucana: sito zamienia wszystko podejrzane
na podkreślenie, więc żadne wejście nie wyprowadzi ścieżki wyżej."""
root = Path(root)
name = _TENANT_OK.sub("_", (tenant or "").strip()).strip("._")
return root / name if name else root
USABLE = frozenset({ACTIVE})
# Stany, o których wolno wiedzieć osobie bez uprawnień administracyjnych.