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>
8.6 KiB
Konta i uprawnienia (PRE-27)
Rozszerzenie kont imiennych z PRE-17: 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.