fd79513ce2
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym długiem, ale niczego nie blokuje. PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała). KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji, których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią kolejność importów. To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa ekranu, którego nie ma. EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną, więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie różne gwarancje, a eksport wynosi najwięcej treści baz naraz. MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma. Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog generowania przeniesiony z app/dodatki na app/moduly/dodatki. ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny (realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności. Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj", w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które audyt podnosił wcześniej. TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował, dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten obraz, i że w nawigacji nie ma martwych odsyłaczy. Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" — i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia, więc sprawdzenie należy do produktu, nie do mechanizmu. Testy: presentation 368, logic 342, data 37, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
7.0 KiB
Python
156 lines
7.0 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
|
|
|
|
from app import rozszerzenia
|
|
|
|
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
|
|
kolejnosc: int = 0 # pozycja w nawigacji; nie zależy od kolejności importów
|
|
|
|
|
|
# ── rejestr: katalog składa się z tego, co zgłoszą ekrany ────────────────
|
|
# Dawniej wszystkie funkcje były wypisane TUTAJ. To znaczyło, że obraz produktu,
|
|
# który części z nich nie ma, i tak niósł ich nazwy — czyli spis funkcji, których
|
|
# nie ma jak włączyć. Teraz ekran zgłasza siebie sam, przy imporcie swojego
|
|
# modułu, więc katalog opisuje dokładnie ten produkt, który się zbudowało.
|
|
#
|
|
# `kolejnosc` jest jawna, bo o zawartości decyduje lista importów w `main.py`,
|
|
# a kolejność importów nie może rządzić kolejnością zakładek w nawigacji.
|
|
SCREENS: list[Feature] = []
|
|
# Pozycje nawigacji dla administratora. Osobno od SCREENS, bo ADMIN nie jest
|
|
# uprawnieniem do nadania — a wpisane na sztywno w szablonie zostawiały nazwę
|
|
# ekranu (i martwy odsyłacz) w produkcie, który tego ekranu nie ma.
|
|
NAWIGACJA_ADMINA: list[tuple[str, str, str]] = [] # (klucz, adres, etykieta)
|
|
EXTRAS: list[Feature] = []
|
|
ALL: tuple[Feature, ...] = ()
|
|
BY_KEY: dict[str, Feature] = {}
|
|
GRANTABLE: frozenset[str] = frozenset()
|
|
|
|
ROUTES: dict[tuple[str, str], str | None] = {}
|
|
STATIC: dict[str, frozenset[str] | str | None] = {}
|
|
|
|
|
|
def _przelicz() -> None:
|
|
global ALL, BY_KEY, GRANTABLE
|
|
SCREENS.sort(key=lambda f: f.kolejnosc)
|
|
EXTRAS.sort(key=lambda f: f.kolejnosc)
|
|
ALL = tuple(SCREENS) + tuple(EXTRAS)
|
|
BY_KEY = {f.key: f for f in ALL}
|
|
GRANTABLE = frozenset(BY_KEY) # ADMIN celowo poza tym zbiorem
|
|
|
|
|
|
def zarejestruj(*, ekran: Feature | None = None, rozszerzenie: Feature | None = None,
|
|
trasy: dict[tuple[str, str], str | None] | None = None,
|
|
zasoby: dict | None = None,
|
|
nawigacja_admina: tuple[str, str, str] | None = None) -> None:
|
|
"""Zgłoszenie funkcji, jej tras i jej zasobów.
|
|
|
|
Idempotentne po kluczu: `export` zgłaszają dwa ekrany (raport do PDF-a
|
|
i wyniki do arkusza), a wpis ma powstać raz."""
|
|
for pozycja, zbior in ((ekran, SCREENS), (rozszerzenie, EXTRAS)):
|
|
if pozycja and pozycja.key not in {f.key for f in zbior}:
|
|
zbior.append(pozycja)
|
|
if nawigacja_admina and nawigacja_admina[0] not in {p[0] for p in NAWIGACJA_ADMINA}:
|
|
NAWIGACJA_ADMINA.append(nawigacja_admina)
|
|
ROUTES.update(trasy or {})
|
|
STATIC.update(zasoby or {})
|
|
_przelicz()
|
|
|
|
|
|
# Funkcje wnoszone przez moduły opcjonalne — zgłaszane tak samo jak ekrany,
|
|
# tylko przez neutralny most, żeby katalog nie musiał ich wymieniać z nazwy.
|
|
for _krotka in rozszerzenia.funkcje():
|
|
zarejestruj(rozszerzenie=Feature(*_krotka))
|
|
zarejestruj(trasy=rozszerzenia.trasy(), zasoby=rozszerzenia.zasoby())
|
|
|
|
|
|
# ── 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.
|
|
|
|
|
|
# ── 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: `compile.js`
|
|
# zdradza składanie raportu, a `wheelzoom.js` — istnienie kosmogramu. 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"})
|
|
|
|
|
|
|
|
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 kolejności zadeklarowanej, nie w tej,
|
|
w której akurat wykonały się importy."""
|
|
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)
|