Files
astrololo/docs/konta-i-uprawnienia.md
T
gitea 320a0ab24e
build-render / build (push) Failing after 8s
build-swisseph / build (push) Successful in 9s
build / build (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 5s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 4s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Failing after 4s
PRE-27: pełne ukrycie niedostępnych funkcji (paranoja)
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>
2026-08-21 17:54:03 +02:00

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.