# Konta i uprawnienia (PRE-27) Rozszerzenie kont imiennych z [PRE-17](konta-i-audyt.md): konta zakłada się **z aplikacji**, a każde dostaje własny zestaw funkcji. ## Dwie zasady, z których wynika reszta **1. Konto ograniczone widzi program KOMPLETNY — tylko mniejszy.** Nic nie może zdradzać, że istnieje coś więcej. Żadnych wyszarzonych zakładek, żadnego „brak uprawnień", żadnego 403 — bo **403 samo w sobie jest informacją**, że pod tym adresem coś jest. Ścieżka bez uprawnienia odpowiada **404**, tak samo jak adres, którego nie ma. Z tej zasady wynikło też wyłączenie `/docs`, `/redoc` i `/openapi.json`. Automatyczna dokumentacja FastAPI wypisuje komplet tras — czyli spis wszystkich funkcji programu. Ochrona zakładek nic by nie dała, gdyby obok leżał ich katalog. (Znalezione testem, nie przeglądem kodu.) **2. Konto administracyjne pochodzi WYŁĄCZNIE ze środowiska.** `APP_USER` / `APP_PASSWORD` (albo `APP_USERS`) — jak dotąd. To konto ma wszystkie uprawnienia i jako jedyne zarządza pozostałymi. **Nie leży w pliku kont**, więc nie da się go skasować ani ograniczyć z ekranu — nawet przez pomyłkę, nawet spreparowanym żądaniem. Konto założone w pliku o tym samym loginie **nie przesłoni** administracyjnego (kolejność sprawdzania jest odwrotna). ## Podział funkcji **Ekrany** — zakładki widoczne w nawigacji: | klucz | zakładka | |---|---| | `chart` | Horoskop | | `interpret` | Interpretacje | | `timeline` | Kalendarz | | `synastry` | Synastria | | `significators` | Sygnifikatory | | `compile` | Skompiluj | | `settings` | Ustawienia | **Rozszerzenia** — poziomy złożoności wewnątrz ekranów: | klucz | co daje | |---|---| | `houses_compare` | wybór systemu domów, zestawienie kilku obok siebie, obrót koła | | `extra_charts` | aspektarian, wykres deklinacji, oś antyscji | | `advanced_calc` | stacje planet, tabele żywiołów i faz, aspekty poboczne, zodiaki syderyczne | | `ai` | horoskopy pisane przez model językowy (**każde użycie kosztuje**) | | `export` | pobieranie PDF i Excela | Konto bez `houses_compare` dostaje horoskop w Whole Sign i **nie widzi**, że systemów jest trzynaście. Konto bez `ai` nie zobaczy przycisku generowania ani nie wywoła go z pominięciem interfejsu. ## Gdzie leży granica W handlerze, nie w szablonie. Ukrycie pola w formularzu chroni przed przypadkiem, ale nie przed kimś, kto zna nazwy pól — dlatego `_limit_options()` ścina opcje **po stronie serwera**, a rysunki dodatkowe bez uprawnienia w ogóle nie powstają (nie ma ich nawet w źródle strony). Mapa `trasa → uprawnienie` jest **jedna**, w `app/features.py`. Rozproszenie jej po dekoratorach kończy się trasą, o której ochronie ktoś zapomniał — a taka dziura jest niewidoczna do chwili, gdy ktoś ją znajdzie. Trasa bez wpisu w mapie wymaga uprawnień administracyjnych: **przeoczenie ma zamykać, nie otwierać**. Test przechodzi po wszystkich trasach aplikacji i wymaga, by każda była opisana. ### Gdy jedna trasa robi kilka rzeczy Mapa tras nie wystarcza tam, gdzie jedna trasa obsługuje kilka funkcji naraz. `POST /interpret` liczy horoskop, ale to samo pole `action` prosi o wygenerowanie promptu, napisanie horoskopu przez model albo eksport arkusza. Konto, które ma mieć Interpretacje bez generowania, musi dostać tę trasę — więc granica przebiega wewnątrz niej, po akcjach: `_AKCJE_POD_UPRAWNIENIEM` przypisuje akcji uprawnienie, a `_dozwolona_akcja()` sprowadza żądanie bez uprawnienia do akcji domyślnej ekranu. Sprowadza — nie odrzuca. Komunikat „brak uprawnień do generowania" sam w sobie mówiłby, że taka funkcja istnieje, czyli łamałby zasadę drugą po to, żeby wyegzekwować pierwszą. Akcja bez uprawnienia ma wyglądać na literówkę w formularzu. ### Ślad to nie tylko przycisk Wymaganie brzmi „nie może być śladu", i to jest mocniejsze niż schowanie kontrolki. Największym wyciekiem po stronie generowania nie był przycisk, tylko **katalog modeli** — nazwy dostawców, nazwy modeli i rozmiary okien kontekstu — wstrzykiwany w stronę blokiem JSON na każdym ekranie z generowaniem, niezależnie od uprawnień. Dlatego `_llm_catalog_for()` oddaje pusty katalog kontu bez uprawnienia, a szablony trzymają pod bramką także znaczniki (`natalNote`, `reportNatal`), pliki skryptów (`models.js`, `progress.js`, `natal.js`, `predictions.js`) i **zdania opisujące funkcję** — podtytuł ekranu Skompiluj wymieniał interpretację od AI z nazwy. Testu na to nie da się napisać przez „sprawdź, czy przycisku nie ma": trzeba sprawdzić, że w źródle strony nie ma żadnego z tych śladów, i mieć kontrolę pozytywną, że przy uprawnieniu wszystkie są. Inaczej test przechodzi także wtedy, gdy generowanie jest zepsute dla wszystkich. ## Pełna paranoja: ukrywanie jest nadrzędne Właściciel produktu postawił to wyżej niż wygodę i wyżej niż czytelność komunikatów: *„nie chcę, żeby osoba wrzucająca bazy wiedziała, po co to robi i jak będzie w przyszłości działał program, bo to rozgada"*. Persona nazywa się **wgrywacz** — konto z uprawnieniami `files` + `files_input` i niczym więcej. Nie ma dowiedzieć się: jakie inne funkcje istnieją, że teksty pisze model językowy i u jakiego dostawcy, do czego służą wgrywane pliki, co jest planowane, że istnieje walidacja plików, ani że istnieje konto, które widzi więcej. ### Wyciek prawie nigdy nie siedzi tam, gdzie się go szuka Audyt sześciu kanałów potwierdził 26 wycieków. Ani jeden nie był przyciskiem. | Kanał | Co wyciekało | |---|---| | `/static/**` poza bramką | komplet skryptów i arkuszy dla **niezalogowanego** | | komentarze w CSS/JS | pełne zdania po polsku o funkcjach, o kwarantannie i o tym, że administrator widzi więcej | | `styles.css` jako jeden plik | nazwy selektorów = spis funkcji programu | | `base.html` | skrypty kosmogramu na **każdej** stronie, łącznie z „przyszłą zakładką" | | komunikaty błędu | nazwa trasy, nazwa podsystemu, nazwa gałęzi rozwojowej, wewnętrzny `host:port` | | komunikat po wgraniu | słowo „administrator" — i **dwie różne treści**, czyli wyrocznia do odgadywania reguł walidacji | | `/openapi.json` warstw wewnętrznych | katalog wszystkich funkcji, bez logowania | ### Trzy zasady, które z tego wynikają **Zasób jest częścią funkcji.** Skrypt i arkusz przechodzą przez tę samą bramkę co ekran (`features.STATIC`). Nazwa pliku jest zgadywalna, więc plik publiczny opowiada o funkcji równie dokładnie jak przycisk. Publiczny został jeden `base.css` — bo potrzebuje go ekran logowania — i dlatego nie wolno w nim umieścić niczego, co nazywa funkcję. **Komentarz nie jedzie na drut.** `_asset_body()` usuwa komentarze przy serwowaniu. Zostają w repozytorium, gdzie są potrzebne. Ta jedna zmiana zamyka cztery z sześciu kanałów naraz. **Różnica jest informacją.** Dwa różne komunikaty po wgraniu pliku były wyrocznią: wystarczyło wgrywać spreparowane pliki i czytać odpowiedź. Teraz komunikat jest jeden, niezależnie od wyniku. Z tego samego powodu odmowa to 404 identyczne z „nie ma takiej trasy", a akcja bez uprawnienia cofa się do domyślnej zamiast tłumaczyć, czego brakuje. ### Zapora słownikowa Łatanie punkt po punkcie przegrywa z następną zmianą. Dlatego `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ść. Dwie z trzech list biorą się wprost z katalogu funkcji — nazwa funkcji, adres jej ekranu i nazwy jej zasobów — więc dopisanie funkcji automatycznie dopisuje je do tego, czego konto bez niej nie może zobaczyć. Trzecia lista, słownictwo dziedziny i mechanizmów, jest pisana ręcznie, bo katalog jej nie zna. Test ma kontrolę pozytywną: dla administratora te same słowa **muszą** się pojawiać. Bez niej przechodziłby także wtedy, gdyby program był pusty. ## Gdzie leżą konta Plik JSON wskazany przez `ACCOUNTS_FILE` (domyślnie `/app/state/accounts.json`), na NFS — **własny podkatalog prezentacji**, nie katalog z bazami: zamontowanie tutaj całego udziału obeszłoby bokiem zamknięcie dostępu z DAN-25. Hasła wyłącznie jako hash scrypt, tym samym mechanizmem co `APP_USERS` — jedna implementacja, więc nie ma czego rozjechać. Zapis jest **atomowy** (plik tymczasowy + `os.replace` w tym samym katalogu): przerwanie zapisu nie obetnie pliku, czyli nie skasuje wszystkich kont naraz. ## Czego ten mechanizm NIE robi Nie zastępuje ochrony baz na poziomie sieci ani NFS (DAN-25). Ktoś z dostępem do udziału albo do warstwy danych nadal je odczyta — uprawnienia w aplikacji ograniczają to, co widać **przez aplikację**, i tyle.