73af7205d1
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m31s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m32s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m29s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 19s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 10s
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>
124 lines
5.7 KiB
Python
124 lines
5.7 KiB
Python
"""Katalog funkcji i uprawnień (PRE-27).
|
|
|
|
DWIE ZASADY, KTÓRE RZĄDZĄ TYM MODUŁEM
|
|
|
|
1. Konto ograniczone ma widzieć program KOMPLETNY — tylko mniejszy. Nic nie może
|
|
sugerować, że istnieje coś więcej: żadnych wyszarzonych pozycji, żadnego
|
|
„brak uprawnień", żadnego 403. Ścieżka bez uprawnienia zwraca **404**, bo 403
|
|
jest samo w sobie informacją, że coś tam jest.
|
|
|
|
2. Mapa trasa → uprawnienie jest JEDNA i leży tutaj. 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. Test przechodzi po
|
|
wszystkich trasach aplikacji i wymaga, żeby każda była tu wymieniona.
|
|
|
|
PODZIAŁ NA GRUPY. Ekrany odpowiadają zakładkom — to naturalna jednostka, bo
|
|
zakładka jest tym, co widać w nawigacji. Rozszerzenia to POZIOMY ZŁOŻONOŚCI
|
|
wewnątrz ekranów: ktoś może liczyć horoskopy, nie mając dostępu do porównywania
|
|
systemów domów ani do generowania tekstu przez model (co kosztuje pieniądze).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
ADMIN = "admin" # zakładanie kont i nadawanie uprawnień — nie do nadania z UI
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Feature:
|
|
key: str
|
|
label: str
|
|
hint: str
|
|
group: str
|
|
href: str = "" # tylko ekrany; rozszerzenia nie mają własnej zakładki
|
|
|
|
|
|
# ── ekrany (zakładki) ────────────────────────────────────────────────────
|
|
SCREENS: tuple[Feature, ...] = (
|
|
Feature("chart", "Horoskop", "Pozycje, osie, domy, aspekty, kosmogram.", "Ekrany", "/"),
|
|
Feature("interpret", "Interpretacje", "Interpretacja natalna z baz i od modelu.", "Ekrany", "/interpret"),
|
|
Feature("timeline", "Kalendarz", "Predykcje na wybrany zakres dat.", "Ekrany", "/timeline"),
|
|
Feature("synastry", "Synastria", "Porównanie dwóch horoskopów.", "Ekrany", "/synastry"),
|
|
Feature("significators", "Sygnifikatory", "Wyszukiwarka po bazach interpretacyjnych.", "Ekrany", "/significators"),
|
|
Feature("compile", "Skompiluj", "Złożenie raportu z policzonych części.", "Ekrany", "/compile"),
|
|
Feature("settings", "Ustawienia", "Podgląd baz i konfiguracji modelu.", "Ekrany", "/settings"),
|
|
)
|
|
|
|
# ── rozszerzenia (poziomy złożoności wewnątrz ekranów) ───────────────────
|
|
EXTRAS: tuple[Feature, ...] = (
|
|
Feature("houses_compare", "Porównanie systemów domów",
|
|
"Wybór systemu innego niż domyślny i zestawienie kilku obok siebie.",
|
|
"Rozszerzenia"),
|
|
Feature("extra_charts", "Wykresy dodatkowe",
|
|
"Aspektarian, wykres deklinacji, oś antyscji.", "Rozszerzenia"),
|
|
Feature("advanced_calc", "Obliczenia zaawansowane",
|
|
"Stacje planet, tabele żywiołów i faz, aspekty poboczne, zodiaki syderyczne.",
|
|
"Rozszerzenia"),
|
|
Feature("ai", "Generowanie tekstu przez model",
|
|
"Horoskopy pisane przez model językowy. UWAGA: każde użycie kosztuje.",
|
|
"Rozszerzenia"),
|
|
Feature("export", "Eksport plików",
|
|
"Pobieranie raportu jako PDF i wyników jako Excel.", "Rozszerzenia"),
|
|
)
|
|
|
|
ALL: tuple[Feature, ...] = SCREENS + EXTRAS
|
|
BY_KEY: dict[str, Feature] = {f.key: f for f in ALL}
|
|
GRANTABLE: frozenset[str] = frozenset(BY_KEY) # ADMIN celowo poza tym zbiorem
|
|
|
|
|
|
# ── mapa trasa → wymagane uprawnienie ────────────────────────────────────
|
|
# None = dostępne każdemu zalogowanemu. Dotyczy to pomocników (geokoder, strefa
|
|
# czasowa, health), które same w sobie NICZEGO nie zdradzają o funkcjach programu.
|
|
ROUTES: dict[tuple[str, str], str | None] = {
|
|
("GET", "/"): None, # korzeń przekierowuje na pierwszy dostępny ekran
|
|
("POST", "/"): "chart",
|
|
("GET", "/interpret"): "interpret",
|
|
("POST", "/interpret"): "interpret",
|
|
("GET", "/timeline"): "timeline",
|
|
("POST", "/timeline"): "timeline",
|
|
("GET", "/synastry"): "synastry",
|
|
("POST", "/synastry"): "synastry",
|
|
("GET", "/significators"): "significators",
|
|
("POST", "/significators"): "significators",
|
|
("GET", "/compile"): "compile",
|
|
("POST", "/compile"): "compile",
|
|
("POST", "/compile/pdf"): "export",
|
|
("GET", "/settings"): "settings",
|
|
("POST", "/horoscope/stream"): "ai",
|
|
("GET", "/accounts"): ADMIN,
|
|
("POST", "/accounts/create"): ADMIN,
|
|
("POST", "/accounts/update"): ADMIN,
|
|
("POST", "/accounts/delete"): ADMIN,
|
|
("GET", "/geocode"): None,
|
|
("GET", "/reverse"): None,
|
|
("GET", "/timezone"): None,
|
|
("GET", "/health"): None,
|
|
}
|
|
|
|
|
|
def required(method: str, path: str) -> str | None:
|
|
"""Uprawnienie wymagane przez trasę. Nieznana trasa → wymaga admina.
|
|
|
|
Domyślną odpowiedzią jest NAJWĘŻSZE uprawnienie, nie najszersze: dopisanie
|
|
trasy bez wpisu w ROUTES ma ją zamknąć, a nie otworzyć. Odwrotny domyślny
|
|
wybór oznaczałby, że każde przeoczenie tworzy dziurę."""
|
|
return ROUTES.get((method.upper(), path), ADMIN)
|
|
|
|
|
|
def can(perms: frozenset[str], feature: str | None) -> bool:
|
|
return feature is None or feature in perms
|
|
|
|
|
|
def screens_for(perms: frozenset[str]) -> list[Feature]:
|
|
"""Zakładki do pokazania w nawigacji — w stałej kolejności katalogu."""
|
|
return [f for f in SCREENS if f.key in perms]
|
|
|
|
|
|
def normalise(keys) -> frozenset[str]:
|
|
"""Zbiór uprawnień z wejścia formularza: tylko znane klucze, bez ADMIN-a.
|
|
|
|
ADMIN odsiewamy TUTAJ, a nie w handlerze: konto administracyjne pochodzi
|
|
wyłącznie z konfiguracji środowiska, więc żadne wysłanie formularza — nawet
|
|
spreparowane — nie może go nadać."""
|
|
return frozenset(k for k in (keys or ()) if k in GRANTABLE)
|