6466ab89a9
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m12s
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 19s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 12m25s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m33s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 16s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
Ekran „Pliki" z trzema poziomami, wpiętymi w kontrolę dostępu z PRE-27:
„files" widzi listę i KLIKANIEM decyduje, z których baz program korzysta,
„files_input" dokłada wgrywanie i ARCHIWIZACJĘ,
administrator kasowanie, przywracanie z archiwum i REGUŁY WALIDACJI.
STAN JEST TERAZ TRWAŁY. DAN-15 trzymał go w zmiennej DISABLED_BASES, bo warstwa
danych nie miała gdzie zapisywać — udział był montowany read-only. Skoro stan ma
być klikany, musi przetrwać restart, więc udział jest zapisywalny, a stan leży
w pliku obok baz (zapis atomowy: plik opisuje CAŁY zbiór, więc obcięcie w połowie
skasowałoby wiedzę o wszystkich naraz). DISABLED_BASES zostaje jako awaryjne
wyłączenie z konfiguracji i odsiewa DODATKOWO — nie odwrotnie, bo inaczej ktoś
z dostępem do ekranu włączyłby bazę wyłączoną świadomie na poziomie wdrożenia.
ARCHIWIZACJA NIE KASUJE. Plik zostaje na dysku, zamrożony, ze znacznikiem czasu;
znika wyłącznie z użytku. To najdalej idąca operacja osoby wgrywającej dane —
kasować może tylko administrator. Test sprawdza, że plik po archiwizacji nadal
istnieje, bo to jest cała istota tej operacji.
WALIDACJA JEST BRAMKĄ DO UŻYTKU, NIE FILTREM NA WEJŚCIU. Plik wgrany zostaje
NIEZALEŻNIE od wyniku — nie tracimy niczego, co ktoś wgrał. Zmienia się tylko to,
czy da się go włączyć. Sprawdzenie biegnie też w chwili włączania, nie tylko przy
wgrywaniu: reguły mogą się zmienić po fakcie.
O WALIDACJI WIE TYLKO ADMINISTRATOR. Pliki wstrzymane są odsiewane W WARSTWIE
DANYCH przy for_admin=False, a nie ukrywane w szablonie — gdyby dochodziły do
przeglądarki, wystarczyłby podgląd źródła, żeby poznać reguły. Odmowa włączenia
wraca do konta bez uprawnień BEZ POWODU, bo powód zdradza regułę. Sekcja reguł
nie trafia nawet do źródła strony. Test parametryzowany po obu niższych poziomach
szuka w odpowiedzi śladów mechanizmu i wymaga, żeby żadnego nie było.
Każdy plik ma policzony sha256 — tożsamość niezależna od nazwy. Wykorzystuje ją
już odrzucanie duplikatów, a w kroku drugim posłuży do pilnowania zgodności
lustra w SQL.
Przy okazji naprawiony błąd, który dopiero co bym wprowadził: Path("") to
Path("."), czyli wartość PRAWDZIWA, więc `Path(os.getenv(...)) or domyślna`
zawsze wybierało pustą zmienną i zapisywało stan do katalogu bieżącego.
Wymaga zapisywalnego udziału — osobny PR w repo deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
140 lines
6.6 KiB
Python
140 lines
6.6 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("files", "Pliki", "Wybór baz, z których korzysta program.", "Ekrany", "/files"),
|
|
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("files_input", "Wgrywanie i archiwizacja baz",
|
|
"Dodawanie nowych plików baz i wycofywanie ich z użytku (plik zostaje, "
|
|
"zamrożony, ze znacznikiem czasu). Kasować może wyłącznie administrator.",
|
|
"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",
|
|
# Zarządzanie plikami baz (DAN-27). Trzy poziomy: „files" wybiera, z czego
|
|
# program korzysta; „files_input" dokłada wgrywanie i archiwizację;
|
|
# kasowanie, przywracanie i REGUŁY WALIDACJI to wyłącznie administrator —
|
|
# o istnieniu walidacji nikt poza nim nie ma skąd wiedzieć.
|
|
("GET", "/files"): "files",
|
|
("POST", "/files/use"): "files",
|
|
("POST", "/files/upload"): "files_input",
|
|
("POST", "/files/archive"): "files_input",
|
|
("POST", "/files/restore"): ADMIN,
|
|
("POST", "/files/delete"): ADMIN,
|
|
("POST", "/files/rules"): ADMIN,
|
|
("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)
|