Commit Graph

5 Commits

Author SHA1 Message Date
gitea f0d07ee8c3 feat(bezpieczeństwo): sesje logowania zamiast HTTP Basic (LOG-34)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m17s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m30s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 5s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 4s
build / build (push) Successful in 7s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m18s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Basic nie miał wylogowania i nie dało się tego obejść: przeglądarka zapamiętuje
hasło i dosyła je SAMA przy każdym żądaniu, więc serwer nie ma czego zapomnieć.
Poprzednia próba (LOG-32) opierała się na nakłonieniu przeglądarki, żeby porzuciła
zapamiętane dane — zachowaniu powszechnym, ale nigdzie nie zapisanym. Teraz to
serwer decyduje, czy dana przeglądarka jest w środku, i może to cofnąć.

TRZY POZIOMY UNIEWAŻNIENIA, celowo rozdzielone, bo każdy kosztuje co innego:
  1. wylogowanie = skasowanie ciasteczka. Natychmiastowe, bez magazynu.
  2. zmiana hasła albo skasowanie konta = odcisk poświadczenia wpisany
     w ciasteczko przestaje pasować. Dzieje się SAMO, bez pamiętania o tym.
     Bez tego odebranie komuś dostępu nie odbierałoby dostępu aż do wygaśnięcia.
  3. „zamknij sesje" z ekranu kont = licznik pokolenia. Jedyny wymagający zapisu,
     więc jedyny opcjonalny: gdy licznika nie ma, poziomy 1 i 2 nadal działają.

KONTO ADMINISTRACYJNE ODSEPAROWANE. Sprawdzane pierwsze i BEZ DOTYKANIA pliku
kont, co daje dwie rzeczy naraz: konto z pliku o tym samym loginie nie przesłoni
administratora, a administrator zaloguje się także wtedy, gdy plik jest uszkodzony
— czyli w jedynej sytuacji, w której ktoś MUSI wejść, żeby to naprawić. Trzymanie
jego stanu w tym samym pliku dawałoby zakleszczenie: nie da się naprawić, bo nie
da się wejść. Jego odpowiednikiem „wyloguj zewsząd" jest zmiana APP_PASSWORD.

KLUCZ WYMAGANY, FAIL-CLOSED. Usługa z kontami, ale bez klucza podpisu, nie
odróżniłaby ważnej sesji od podrobionej, więc nie wstaje — i lepiej przy starcie
niż przy pierwszym logowaniu człowieka. Losowanie klucza byłoby wygodne, ale
wylogowywałoby wszystkich przy każdym restarcie poda: wygląda jak awaria i uczy
ludzi ignorować ekran logowania.

Ciasteczko HttpOnly (jeden wstrzyknięty skrypt inaczej wynosi sesję) i
SameSite=Strict (obca strona nie zadziała w imieniu zalogowanego). Wylogowanie
POST-em, nie odsyłaczem: pod adresem GET wystarczyłby obrazek na obcej stronie.
Adres powrotu po zalogowaniu przechodzi przez sito — bez tego `?dokad=https://obcy`
zamieniłby nasz ekran logowania w narzędzie do wyłudzania haseł.

Kres bezczynności 8 h i twardy 30 dni. Znacznik aktywności odświeżany z progiem,
inaczej Set-Cookie leciałby przy każdym obrazku i arkuszu stylów.

Zdarzenia logowania w dzienniku (PRE-17). Nieudane próby są tam ważniejsze od
udanych: pojedyncza nic nie znaczy, ale seria pod jednym adresem to jedyny
widoczny ślad zgadywania haseł. Test pilnuje, że hasło tam nie trafia.

Sprawdzone w przeglądarce: po zalogowaniu ciasteczko jest NIEWIDOCZNE dla
JavaScriptu, a po wylogowaniu wejście na chronioną stronę ląduje na ekranie
logowania — czyli dokładnie to, czego Basic nie potrafił.

Testy: 21 na rdzeń podpisywania (w tym podrabianie ładunku, podpisu i klucza,
ciasteczko z przyszłości, śmieci na wejściu), reszta przepisana z Basic na sesje.
Prezentacja 338 zielonych.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:18:15 +02:00
gitea 71bb3b9c0a feat(bezpieczeństwo): przycisk wylogowania (LOG-32)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m17s
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 / Build obrazu silnika B (swisseph) (pull_request) Successful in 5s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 4s
build / build (push) Successful in 1m18s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 5s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Aplikacja nie miała jak z siebie wyjść: raz podane hasło działało do zamknięcia
przeglądarki, a na wspólnym komputerze nie było sposobu, żeby oddać ekran komuś
innemu.

HTTP BASIC NIE MA PRAWDZIWEGO WYLOGOWANIA i to jest sedno tej zmiany. Nie ma
sesji do skasowania: przeglądarka zapamiętuje login i hasło, po czym dosyła je
SAMA przy każdym żądaniu. Serwer nie ma czego zapomnieć — kolejne kliknięcie
przyszłoby z kompletem poświadczeń i weszłoby z powrotem.

Działa natomiast doprowadzenie do tego, żeby to PRZEGLĄDARKA porzuciła zapamiętane
dane, i robimy to dwutorowo, bo ani jedno, ani drugie osobno nie wystarcza:

  1. /wyloguj odpowiada ZAWSZE 401 z nagłówkiem WWW-Authenticate, co wymusza
     ponowne pytanie o hasło. Działa bez JavaScriptu, ale samo w sobie zostawia
     stare dane w pamięci przeglądarki: po anulowaniu okienka wystarczyłoby wejść
     na dowolny adres, żeby wrócić do środka.
  2. wyloguj.js wysyła żądanie z CELOWO błędnymi danymi, którym przeglądarka
     nadpisuje swój wpis. To jest część, która faktycznie czyści pamięć — ale
     opiera się na zachowaniu powszechnym, a nie zapisanym w standardzie, więc
     nie może być jedynym mechanizmem.

Strona wylogowania mówi wprost, że pewnym sposobem w KAŻDEJ przeglądarce jest
zamknięcie okna. Nie obiecujemy więcej, niż Basic potrafi — obietnica bez pokrycia
byłaby tu gorsza od braku przycisku, bo dawałaby złudzenie, że ekran jest oddany.

Trasa stoi POZA bramką logowania, celowo: inaczej dostałaby 200 od zalogowanej
sesji i nie miałaby jak odpowiedzieć 401. Odpowiada też bez logowania — inaczej
wyjście wymagałoby bycia w środku, co jest błędnym kołem. Nagłówki zakazują
zapamiętania strony, bo oddana z pamięci podręcznej nie dotarłaby do serwera
i okienko w ogóle by się nie pojawiło.

Obok wyjścia pokazujemy, KTO jest zalogowany: bez tego przycisk jest w połowie
bezużyteczny, bo na wspólnym komputerze nie wiadomo, kogo się wylogowuje. Przy
wyłączonym logowaniu nie ma ani jednego, ani drugiego — przycisk sugerowałby
ochronę, której nie ma.

Sprawdzone w przeglądarce: oba żądania wychodzą (to z błędnymi danymi i samo
przejście), oba wracają 401, strona renderuje się poprawnie. Samego unieważnienia
pamięci poświadczeń NIE dało się tu potwierdzić — wymaga okienka systemowego,
którego automat nie obsłuży.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 23:52:04 +02:00
gitea ee3c515d59 fix(testy): awaria magazynu kont wymuszana podmianą, nie prawami pliku
build / build (push) Successful in 7s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Dwa testy z PR #71 przechodziły lokalnie i padały w CI. Przyczyna nie miała nic
wspólnego z badaną rzeczą: wymuszały awarię przez chmod 000 i chmod 555, a CI
działa jako ROOT — root omija bity uprawnień w Linuksie, więc odczyt i zapis się
udawały i asercje leciały na komunikat, którego nie było.

Test zależny od tego, KTO go uruchamia, jest gorszy niż jego brak: daje fałszywe
poczucie pokrycia i zapala się w miejscu niezwiązanym z tym, co sprawdza.

Teraz awarię wymuszamy podmianą dokładnie tego punktu, w którym system plików
mówi „nie": odczytu pliku kont (osłona przepuszcza wszystko inne, żeby szablony
nadal się wczytywały) oraz mkstemp przy zapisie. To drugie jest celowe — mkstemp
wywala się PIERWSZY przy katalogu tylko do odczytu, jeszcze zanim dojdzie do
zapisu i podmiany, i właśnie tego dotyczyła naprawiana poprawka.

Sprawdzian na PRAWDZIWYCH prawach zostaje, ale z pominięciem przy uruchomieniu
z roota — jako kontrola, że podmiana odpowiada rzeczywistości, a nie tylko sama
sobie. W CI się nie wykona i to jest w porządku: znaczyłby tam tyle co nic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:38:14 +00:00
gitea e2b50b284f fix(konta): awaria magazynu tłumaczy się zamiast dawać gołe 500
Testy / Testy warstwy logicznej (silnik) (push) Successful in 26m56s
build / build (push) Successful in 9s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4m54s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Ekran „Konta" wywalał się na produkcji błędem 500 bez słowa wyjaśnienia.
Odtworzone lokalnie: `_read()` łapał wyłącznie brak pliku i zły JSON, więc każdy
inny błąd systemu plików — a na udziale NFS to głównie prawa — leciał na wierzch
jako nieobsłużony wyjątek.

To jest szczególnie zły sposób na awarię AKURAT TUTAJ: ekran kont jest jedynym
miejscem, z którego administrator może taki problem naprawić, a gołe 500 nie mówi
mu ani co, ani gdzie.

Teraz każdy błąd magazynu ma twarz: osobny wyjątek AccountsUnavailable niosący
ŚCIEŻKĘ i powód z systemu operacyjnego, plus podpowiedź najczęstszej przyczyny
(prawa katalogu na udziale albo wolumen zamontowany tylko do odczytu). Strona
renderuje się normalnie z tym komunikatem u góry.

Objęte są wszystkie cztery drogi zapisu, a nie tylko odczyt. W szczególności
mkstemp: przy katalogu tylko do odczytu wywala się ONO pierwsze, jeszcze zanim
dojdzie do zapisu i podmiany — więc obudowanie samego os.replace nic by nie dało
(złapane testem, nie przeglądem kodu).

USZKODZONY PLIK NIE JEST NADPISYWANY. Wcześniej niepoprawny JSON dawał pusty
zbiór kont, co przy pierwszym zapisie skasowałoby WSZYSTKIE konta bez śladu.
Teraz to odmowa z komunikatem — plik zostaje nietknięty, a test tego pilnuje.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 20:42:16 +00:00
gitea a833965909 feat(bezpieczeństwo): konta z uprawnieniami do zakładek i funkcji (PRE-27)
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m58s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 3m37s
Testy / Kontrola składni wszystkich warstw (push) Successful in 9s
build / build (push) Successful in 18s
Ekran „Konta" dla administratora: zakładanie, kasowanie i nadawanie uprawnień.
Zestaw funkcji zależy od konta, a konto ograniczone widzi program KOMPLETNY —
tylko mniejszy.

PODZIAŁ NA GRUPY. Ekrany to zakładki (7), bo zakładka jest naturalną jednostką —
to ją widać w nawigacji. Rozszerzenia to POZIOMY ZŁOŻONOŚCI wewnątrz ekranów:
porównanie systemów domów, wykresy dodatkowe, obliczenia zaawansowane, generowanie
tekstu przez model (kosztuje pieniądze) i eksport plików. Konto bez porównania
domów dostaje horoskop w Whole Sign i nie wie, że systemów jest trzynaście.

NIC NIE ZDRADZA, ŻE JEST WIĘCEJ:
- brak pozycji w menu zamiast pozycji wyszarzonej,
- 404 zamiast 403 — odmowa z powodem sama mówi, że coś tam jest,
- rysunki bez uprawnienia w OGÓLE NIE POWSTAJĄ, więc nie ma ich nawet w źródle,
- automatyczna dokumentacja API wyłączona. /docs, /redoc i /openapi.json wypisują
  komplet tras, czyli spis wszystkich funkcji programu — ochrona zakładek nic by
  nie dała, gdyby obok leżał ich katalog. Znalezione TESTEM przechodzącym po
  trasach aplikacji, nie przeglądem kodu.

KONTO ADMINISTRACYJNE zostaje w APP_USER/APP_PASSWORD, jak było. Nie leży w pliku
kont, więc nie da się go skasować ani ograniczyć z ekranu. Konto założone w pliku
o tym samym loginie NIE przesłoni administracyjnego — kolejność sprawdzania jest
odwrotna, inaczej dałoby się odebrać uprawnienia jedynemu, kto może je nadawać.
Uprawnienia administracyjnego nie da się też nadać z formularza: odsiewamy je
w normalise(), a nie w handlerze, więc żadne spreparowane żądanie tam nie sięgnie.

GRANICA JEST W HANDLERZE, NIE W SZABLONIE. Ukrycie pola chroni przed przypadkiem,
nie przed kimś, kto zna nazwy pól — _limit_options() ścina opcje po stronie
serwera i test wysyła spreparowane żądanie, żeby to potwierdzić.

MAPA TRASA→UPRAWNIENIE JEST JEDNA (features.ROUTES). Rozproszenie jej po
dekoratorach kończy się trasą, o której ochronie ktoś zapomniał — a taka dziura
jest niewidoczna, dopóki ktoś jej nie znajdzie. Trasa bez wpisu wymaga
administratora: przeoczenie ma ZAMYKAĆ, nie otwierać. Test idzie po trasach
APLIKACJI, nie po wpisach mapy — inaczej potwierdzałby tylko sam siebie.

Konta w pliku JSON na własnym podkatalogu NFS (nie tam, gdzie bazy — zamontowanie
całego udziału obeszłoby bokiem DAN-25). Hasła wyłącznie jako hash scrypt, tym
samym mechanizmem co APP_USERS. Zapis atomowy, bo przerwanie zapisu na NFS
obcięłoby plik, czyli skasowało wszystkie konta naraz.

Przy okazji przepisane trzy testy, które greppowały nawigację i main.py: menu
powstaje teraz z katalogu funkcji, więc szukanie sztywnych linków w base.html
niczego już nie sprawdzało.

Wymaga wolumenu na konta — osobny PR w repo deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:18:14 +00:00