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
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>
206 lines
9.5 KiB
Python
206 lines
9.5 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,
|
|
# Wylogowanie musi być dostępne dla każdego zalogowanego, niezależnie od tego,
|
|
# co mu przyznano — konto bez ani jednego ekranu też ma prawo wyjść.
|
|
("GET", "/logowanie"): None,
|
|
("POST", "/logowanie"): None,
|
|
("POST", "/wyloguj"): None,
|
|
("POST", "/accounts/sesje"): ADMIN,
|
|
}
|
|
|
|
|
|
# ── zasoby statyczne ─────────────────────────────────────────────────────
|
|
# Skrypt i arkusz stylów są CZĘŚCIĄ funkcji, nie jej dekoracją. Nazwa pliku jest
|
|
# zgadywalna, a treść opowiada o funkcji dokładniej niż przycisk: `models.js`
|
|
# wymienia dostawców modeli, `compile.js` — składanie raportu. Dlatego każdy
|
|
# zasób przechodzi przez tę samą bramkę co ekrany.
|
|
#
|
|
# Wartość to ZBIÓR uprawnień, z których wystarczy JEDNO: wyszukiwarka lokalizacji
|
|
# siedzi na trzech ekranach i konto z którymkolwiek z nich musi ją dostać.
|
|
# `None` = każdy zalogowany. Zasób spoza mapy wymaga admina — jak trasa.
|
|
_FORMULARZE = frozenset({"chart", "interpret", "timeline", "compile"})
|
|
_LOKALIZACJA = frozenset({"chart", "interpret", "timeline"})
|
|
_KOLO = frozenset({"chart", "compile"})
|
|
|
|
STATIC: dict[str, frozenset[str] | str | None] = {
|
|
# arkusze ekranów — jeden ekran, jedno uprawnienie
|
|
"s-chart.css": "chart",
|
|
"s-interpret.css": "interpret",
|
|
"s-timeline.css": "timeline",
|
|
"s-synastry.css": "synastry",
|
|
"s-significators.css": "significators",
|
|
"s-compile.css": "compile",
|
|
"s-files.css": "files",
|
|
"s-settings.css": "settings",
|
|
"s-accounts.css": ADMIN,
|
|
"x-ai.css": "ai",
|
|
# skrypty
|
|
"formsync.js": _FORMULARZE,
|
|
"now.js": _LOKALIZACJA,
|
|
"geo.js": _LOKALIZACJA,
|
|
"vendor/leaflet/leaflet.js": _LOKALIZACJA,
|
|
"vendor/leaflet/leaflet.css": _LOKALIZACJA,
|
|
"wheelzoom.js": _KOLO,
|
|
"wheeltip.js": _KOLO,
|
|
"copy.js": frozenset({"interpret", "timeline"}),
|
|
"compile.js": "compile",
|
|
"models.js": "ai",
|
|
"progress.js": "ai",
|
|
"natal.js": "ai",
|
|
"predictions.js": "ai",
|
|
# base.css celowo NIE MA tu wpisu: potrzebuje go ekran logowania, więc jest
|
|
# publiczny (PUBLIC_PATHS w security.py). Dlatego nie wolno w nim trzymać
|
|
# niczego, co nazywa jakąkolwiek funkcję.
|
|
}
|
|
|
|
|
|
def static_required(name: str) -> frozenset[str] | str | None:
|
|
"""Uprawnienie dla zasobu. Nieznany zasób → admin, jak nieznana trasa."""
|
|
return STATIC.get(name, ADMIN)
|
|
|
|
|
|
def required(method: str, path: str) -> frozenset[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ę."""
|
|
if path.startswith("/static/"):
|
|
return static_required(path[len("/static/"):])
|
|
return ROUTES.get((method.upper(), path), ADMIN)
|
|
|
|
|
|
def can(perms: frozenset[str], feature: frozenset[str] | str | None) -> bool:
|
|
"""Czy te uprawnienia otwierają tę funkcję.
|
|
|
|
Zbiór znaczy „wystarczy którekolwiek": jeden zasób bywa wspólny dla kilku
|
|
ekranów i nie ma powodu wymagać ich wszystkich naraz."""
|
|
if feature is None:
|
|
return True
|
|
if isinstance(feature, str):
|
|
return feature in perms
|
|
return bool(perms & feature)
|
|
|
|
|
|
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)
|