diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 2d0f319..8d9d9b0 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -9,12 +9,12 @@ jobs: - uses: actions/checkout@v4 - name: Login run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.czernobog.pl -u gitea --password-stdin - # astroklient-demo dołącza do tej samej pętli: dzieli warstwę logiczną i łącze, + # astrodemo dołącza do tej samej pętli: dzieli warstwę logiczną i łącze, # więc jego obraz ma powstawać z tego samego commita co reszta produktu. - - name: Build & push (data, logic, presentation, astroklient-demo) + - name: Build & push (data, logic, presentation, astrodemo) run: | TAG=${GITHUB_SHA::8} - for SVC in data logic presentation astroklient-demo; do + for SVC in data logic presentation astrodemo; do docker build -t gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG ./services/$SVC docker push gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG done diff --git a/.gitea/workflows/tests.yml b/.gitea/workflows/tests.yml index 2021e5a..c102699 100644 --- a/.gitea/workflows/tests.yml +++ b/.gitea/workflows/tests.yml @@ -91,8 +91,8 @@ jobs: PYTHONPATH: . run: pytest tests -q -rs - astroklient-demo-tests: - name: Testy astroklienta-demo + astrodemo-tests: + name: Testy astrodemo runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -100,14 +100,14 @@ jobs: with: python-version: "3.12" cache: pip - cache-dependency-path: services/astroklient-demo/requirements-dev.txt + cache-dependency-path: services/astrodemo/requirements-dev.txt - name: Instalacja zależności - run: pip install -r services/astroklient-demo/requirements-dev.txt + run: pip install -r services/astrodemo/requirements-dev.txt # Demo rozmawia z warstwą danych PRODUKCJI, więc jego powierzchnia musi być # pilnowana tak samo jak reszty: testy sprawdzają m.in., że nie przybyła # żadna trasa poza dwiema funkcjami. - name: Testy (pytest) - working-directory: services/astroklient-demo + working-directory: services/astrodemo env: PYTHONPATH: . run: pytest tests -q -rs diff --git a/services/astroklient-demo/Dockerfile b/services/astrodemo/Dockerfile similarity index 100% rename from services/astroklient-demo/Dockerfile rename to services/astrodemo/Dockerfile diff --git a/services/astroklient-demo/README.md b/services/astrodemo/README.md similarity index 84% rename from services/astroklient-demo/README.md rename to services/astrodemo/README.md index 9d381af..4a0999e 100644 --- a/services/astroklient-demo/README.md +++ b/services/astrodemo/README.md @@ -1,4 +1,4 @@ -# astroklient-demo — wersja demonstracyjna (PRE-28) +# astrodemo — wersja demonstracyjna (PRE-28) Osobna warstwa prezentacji o **dwóch funkcjach**: @@ -18,7 +18,7 @@ okazji kodu reszty programu. ## Izolacja: własne dane, pula na konto ``` -astroklient-demo → logic-demo → data-demo → udział astrololo-demo (pusty na starcie) +astrodemo → logic-demo → data-demo → udział astrololo-demo (pusty na starcie) └── klientA/ ← pula konta └── klientB/ ← pula konta ``` @@ -42,15 +42,15 @@ w nazwie katalogu: `../..` czy ukośnik wyprowadziłyby zapytanie poza udział. **Pula klienta to jeden katalog**, więc przejście na pełną wersję nie oznacza utraty wgrań — procedura importu jest w runbooku wdrożenia (repo `deploy`, -`astrololo/README-astroklient-demo.md`). +`astrololo/README-astrodemo.md`). ## Konfiguracja | zmienna | znaczenie | |---|---| | `LOGIC_URL` | adres warstwy logicznej (domyślnie `http://localhost:8001`) | -| `DEMO_USERS` | konta jako `login:sekret`, po przecinku. Sekret jawny albo hash `scrypt$sól$hash`. Każdy login = osobna pula plików | -| `DEMO_USER` / `DEMO_PASSWORD` | pojedyncze konto — zgodność wstecz, działa gdy `DEMO_USERS` jest puste. **Brak obu = aplikacja stoi otwarta** i mówi o tym głośno przy starcie | +| `ASTRODEMO_USERS` | konta jako `login:sekret`, po przecinku. Sekret jawny albo hash `scrypt$sól$hash`. Każdy login = osobna pula plików | +| `ASTRODEMO_USER` / `ASTRODEMO_PASSWORD` | pojedyncze konto — zgodność wstecz, działa gdy `ASTRODEMO_USERS` jest puste. **Brak obu = aplikacja stoi otwarta** i mówi o tym głośno przy starcie | | `INTERNAL_TOKEN` | token międzywarstwowy (LOG-32) — ten sam co reszta usług | | `LINK_KEY_PRESENTATION_LOGIC` | klucz szyfrowanego łącza (PRE-16) — ten sam co prezentacja | | `RATE_LIMIT_PER_MIN` | limit żądań na adres, domyślnie 60; `0` wyłącza | @@ -66,16 +66,16 @@ cd services/presentation && python scripts/make_user.py demo ## Uruchomienie lokalne ```bash -cd services/astroklient-demo +cd services/astrodemo pip install -r requirements-dev.txt -DEMO_PASSWORD=tajne LOGIC_URL=http://localhost:8001 \ +ASTRODEMO_PASSWORD=tajne LOGIC_URL=http://localhost:8001 \ uvicorn app.main:app --port 8005 ``` Testy: ```bash -cd services/astroklient-demo && PYTHONPATH=. pytest tests -q +cd services/astrodemo && PYTHONPATH=. pytest tests -q ``` ## Co pilnują testy diff --git a/services/astroklient-demo/app/__init__.py b/services/astrodemo/app/__init__.py similarity index 100% rename from services/astroklient-demo/app/__init__.py rename to services/astrodemo/app/__init__.py diff --git a/services/astroklient-demo/app/clients/__init__.py b/services/astrodemo/app/clients/__init__.py similarity index 100% rename from services/astroklient-demo/app/clients/__init__.py rename to services/astrodemo/app/clients/__init__.py diff --git a/services/astroklient-demo/app/clients/logic_client.py b/services/astrodemo/app/clients/logic_client.py similarity index 89% rename from services/astroklient-demo/app/clients/logic_client.py rename to services/astrodemo/app/clients/logic_client.py index bddb49a..13cdadf 100644 --- a/services/astroklient-demo/app/clients/logic_client.py +++ b/services/astrodemo/app/clients/logic_client.py @@ -1,8 +1,8 @@ """Klient warstwy logicznej — CELOWO okrojony do dwóch funkcji (PRE-28). -Astroklient nie dziedziczy klienta z pełnej prezentacji i nie importuje go: ma tu -być wyłącznie to, czego naprawdę używa. Metoda, której nie ma, nie zostanie -przypadkiem zawołana ani nie podpowie, że po drugiej stronie jest coś więcej. +Ma tu być wyłącznie to, czego ta usługa naprawdę używa. Metoda, której nie ma, +nie zostanie przypadkiem zawołana ani nie podpowie, że po drugiej stronie jest +coś więcej. Rozmowa idzie tym samym szyfrowanym łączem co reszta (PRE-16) i pod tym samym tokenem międzywarstwowym (LOG-32) — demo nie jest furtką omijającą ochronę. @@ -45,8 +45,8 @@ class LogicClient: def report(self, when_utc_iso: str, lat: float, lon: float) -> dict[str, Any]: """Interpretacja urodzeniowa: sygnifikatory z obliczeń szukane w bazach. - Jedyne pytanie, jakie astroklient-demo zadaje o treść — bez grupowania, - bez promptów, bez modelu językowego.""" + Jedyne pytanie, jakie astrodemo zadaje o treść — bez grupowania, + bez grupowania i bez żadnej obróbki treści.""" payload = {"when_utc": when_utc_iso, "lat": lat, "lon": lon, "limit": settings.report_limit, "group": False, "tenant": self.tenant} diff --git a/services/astroklient-demo/app/config.py b/services/astrodemo/app/config.py similarity index 79% rename from services/astroklient-demo/app/config.py rename to services/astrodemo/app/config.py index b02ed6a..c88142a 100644 --- a/services/astroklient-demo/app/config.py +++ b/services/astrodemo/app/config.py @@ -1,7 +1,6 @@ -"""Konfiguracja astroklienta-demo — wersji demonstracyjnej (PRE-28). +"""Konfiguracja astrodemo — wersji demonstracyjnej (PRE-28). -Zna WYŁĄCZNIE adres warstwy logicznej, tak samo jak pełna prezentacja. Nie wie -nic o bazach, Excelu ani SQL-u. +Zna WYŁĄCZNIE adres warstwy logicznej. Nie wie nic o bazach, Excelu ani SQL-u. """ from __future__ import annotations diff --git a/services/astroklient-demo/app/link_crypto.py b/services/astrodemo/app/link_crypto.py similarity index 99% rename from services/astroklient-demo/app/link_crypto.py rename to services/astrodemo/app/link_crypto.py index 89ed3d8..da99bc2 100644 --- a/services/astroklient-demo/app/link_crypto.py +++ b/services/astrodemo/app/link_crypto.py @@ -153,7 +153,7 @@ class Link: def open(self, direction: bytes, path: str, stamp: str, seq: int, frame: bytes) -> bytes: if not frame.startswith(MAGIC): - raise LinkError("ramka bez znacznika astrololo") + raise LinkError("ramka bez znacznika protokołu") body = frame[len(MAGIC):] if len(body) <= NONCE_BYTES: raise LinkError("ramka za krótka") diff --git a/services/astroklient-demo/app/main.py b/services/astrodemo/app/main.py similarity index 61% rename from services/astroklient-demo/app/main.py rename to services/astrodemo/app/main.py index 14a1d10..e90a9e7 100644 --- a/services/astroklient-demo/app/main.py +++ b/services/astrodemo/app/main.py @@ -1,42 +1,86 @@ -"""astroklient-demo — wersja DEMONSTRACYJNA (PRE-28). +"""astrodemo — wersja DEMONSTRACYJNA (PRE-28). DWIE FUNKCJE I ANI JEDNEJ WIĘCEJ: 1. dodanie pliku bazy (wgranie + włączenie do użytku), 2. zapytanie o interpretację urodzeniową dla podanych danych wejściowych. -DLACZEGO OSOBNA USŁUGA, A NIE KONTO Z OGRANICZENIAMI. Mechanizm uprawnień z PRE-27 -umiałby to ukryć w pełnej aplikacji, ale ukrycie a nieobecność to dwie różne rzeczy. -Tutaj pozostałych funkcji NIE MA W OBRAZIE: nie ma tras, nie ma szablonów, nie ma -nawet metod w kliencie warstwy logicznej. Demo można komuś oddać, nie oddając -przy okazji kodu reszty programu. +DLACZEGO OSOBNA USŁUGA, A NIE KONTO Z OGRANICZENIAMI. Ukrycie a nieobecność to +dwie różne rzeczy. Tutaj niczego poza tymi dwiema funkcjami NIE MA W OBRAZIE: nie +ma tras, nie ma szablonów, nie ma nawet metod w kliencie warstwy logicznej. Obraz +tej usługi można komuś oddać i nie oddaje się przy tym niczego ponad to, co widać +na ekranie — łącznie z komentarzami w tym pliku, bo one też jadą razem z obrazem. -UWAGA, ŚWIADOMA DECYZJA WŁAŚCICIELA: astroklient-demo pracuje na TEJ SAMEJ warstwie -danych co produkcja. Kto ma do niego dostęp, czyta oryginalne bazy interpretacyjne, -a pliki przez niego wgrane trafiają do produkcyjnego zbioru. Dlatego konto jest -osobne (DEMO_USER/DEMO_PASSWORD) — żeby dało się odciąć demo jedną zmienną, bez +PULE SĄ ROZDZIELONE (PRE-29). Każde konto ma własny, izolowany zbiór plików, +niewidoczny dla pozostałych kont; pula bierze się WYŁĄCZNIE z loginu zalogowanej +osoby, nigdy z pola formularza. Konta są osobne (ASTRODEMO_USERS) — żeby dało się +odciąć tę usługę jedną zmienną, bez ruszania kont głównej aplikacji. """ from __future__ import annotations import base64 +import logging +import re +from functools import lru_cache +from pathlib import Path from datetime import datetime, timedelta, timezone import httpx -from fastapi import FastAPI, File, Form, Request, UploadFile -from fastapi.responses import HTMLResponse, RedirectResponse -from fastapi.staticfiles import StaticFiles +from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile +from fastapi.responses import HTMLResponse, RedirectResponse, Response from fastapi.templating import Jinja2Templates from app import link_crypto, security, session from app.clients.logic_client import LogicClient -# Automatyczna dokumentacja WYŁĄCZONA — tak samo jak w pełnej aplikacji. /docs -# wypisałoby komplet tras, a demo ma nie zdradzać nawet własnej powierzchni. -app = FastAPI(title="astroklient-demo", docs_url=None, redoc_url=None, openapi_url=None) +# Automatyczna dokumentacja WYŁĄCZONA. /docs wypisałoby komplet tras, a ta usługa +# ma nie zdradzać nawet własnej powierzchni. +app = FastAPI(title="astrodemo", docs_url=None, redoc_url=None, openapi_url=None) security.install(app) -app.mount("/static", StaticFiles(directory="app/static"), name="static") +# ── zasoby (PRE-27) ──────────────────────────────────────────────────────── +# Katalog NIE jest montowany. Montowanie serwuje wszystko, co w nim leży, każdemu +# i bez śladu w dzienniku — a komentarz w arkuszu stylów mówił wprost, że istnieje +# więcej, niż widać na ekranie. Tu jest jawna lista: arkusz logowania jest +# publiczny, bo +# potrzebuje go strona logowania, a cokolwiek innego jest nieznane i nie istnieje. +ZASOBY_PUBLICZNE = frozenset({"styles.css"}) +_KATALOG_ZASOBOW = Path("app/static") + + +def _bez_komentarzy(tresc: str) -> str: + """Komentarz w pliku statycznym jedzie do przeglądarki bajt w bajt. + W repozytorium zostaje — do odpowiedzi nie ma po co trafiać.""" + tresc = re.sub(r"/\*.*?\*/", "", tresc, flags=re.S) + return re.sub(r"\n{3,}", "\n\n", tresc).strip() + "\n" + + +@lru_cache(maxsize=None) +def _zasob(nazwa: str) -> bytes | None: + """Treść zasobu tak, jak pójdzie na drut — albo None, gdy go nie ma.""" + if nazwa not in ZASOBY_PUBLICZNE: + return None + sciezka = (_KATALOG_ZASOBOW / nazwa).resolve() + # Wyjście poza katalog: `..` w ścieżce albo dowiązanie na zewnątrz. + if _KATALOG_ZASOBOW.resolve() not in sciezka.parents or not sciezka.is_file(): + return None + if sciezka.suffix in (".css", ".js"): + return _bez_komentarzy(sciezka.read_text(encoding="utf-8")).encode("utf-8") + return sciezka.read_bytes() + + +@app.get("/static/{nazwa:path}") +def zasob(nazwa: str): + tresc = _zasob(nazwa) + if tresc is None: + raise HTTPException(status_code=404) + typ = "text/css; charset=utf-8" if nazwa.endswith(".css") else "application/octet-stream" + return Response(tresc, media_type=typ, + headers={"Cache-Control": "public, max-age=3600"}) + templates = Jinja2Templates(directory="app/templates") +# Szczegóły awarii idą TUTAJ, nie na ekran (patrz _blad). +log = logging.getLogger("astrodemo") templates.env.globals["current_user"] = lambda request: getattr(request.state, "user", "") or "" templates.env.globals["auth_on"] = security.auth_enabled @@ -50,7 +94,7 @@ def _logic(request: Request) -> LogicClient: return LogicClient(tenant=getattr(request.state, "user", "") or "") # Stan pliku „w użyciu" — ta sama nazwa, co w warstwie danych (DAN-27). Wpisana -# tutaj wprost, bo astroklient-demo nie importuje tamtej warstwy; literówka objawiłaby +# tutaj wprost, bo astrodemo nie importuje tamtej warstwy; literówka objawiłaby # się dopiero na żywo, więc pilnuje jej test. ACTIVE = "active" @@ -68,15 +112,16 @@ def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]: return moment.astimezone(timezone.utc).isoformat(), local.strftime("%Y-%m-%d %H:%M") +# Jedno zdanie dla wszystkich awarii i CELOWO żadnego szczegółu. Treść wyjątku +# niosła nazwę usługi, jej port i nazwy jej tras — czyli mapę tego, co stoi za tym +# ekranem, podaną komuś, kto ma widzieć wyłącznie dwa formularze. +_AWARIA = "Nie udało się teraz wykonać tej operacji. Spróbuj ponownie za chwilę." + + def _blad(e: Exception) -> str: - if isinstance(e, httpx.HTTPStatusError): - try: - return str(e.response.json().get("detail") or e) - except Exception: # noqa: BLE001 - return str(e) - if isinstance(e, link_crypto.LinkError): - return f"Łącze do warstwy logicznej nie działa: {e}" - return f"Warstwa logiczna niedostępna: {e}" + """Komunikat dla użytkownika. Diagnostyka idzie do dziennika, nie na ekran.""" + log.warning("operacja nieudana: %s: %s", type(e).__name__, e) + return _AWARIA def _ile_rekordow(report: dict) -> int: @@ -113,13 +158,12 @@ def interpretacja(request: Request, date: str = Form(...), time: str = Form(...) async def dodaj_plik(request: Request, upload: UploadFile = File(...)): """Dodanie pliku bazy: wgranie ORAZ włączenie do użytku. - W pełnej aplikacji to dwie osobne decyzje (DAN-27), bo tam ktoś nad tym panuje. - Tutaj „dodać do bazy" ma znaczyć, że plik od razu bierze udział w wyszukiwaniu — - inaczej po wgraniu nic by się nie zmieniło i demo wyglądałoby na zepsute. + Jedna czynność, nie dwie: „dodać plik" ma tu znaczyć, że od razu bierze udział + w wyszukiwaniu — inaczej po wgraniu nic by się nie zmieniło i wyglądałoby to + na zepsute. - Walidacja zostaje: plik o złym układzie NIE wchodzi do użytku, ale też NIE JEST - tracony — leży dalej na udziale i decyzję o nim podejmuje administrator - w pełnej aplikacji.""" + Plik o złym układzie nie wchodzi do użytku, ale NIE JEST tracony — zostaje + zapisany i czeka.""" raw = await upload.read() if not raw: return RedirectResponse("/?error=Pusty+plik.", status_code=303) @@ -153,10 +197,9 @@ def _wroc(error: str = "", done: str = "") -> RedirectResponse: # ---------------- Logowanie i wylogowanie (LOG-34) ---------------- -# Ta sama konstrukcja co w pełnej aplikacji. Różnica jest jedna i wynika z tego, -# że demo nie ma własnego wolumenu: nie ma licznika pokolenia sesji, więc -# „wyrzuć zewsząd" robi się zmianą hasła w DEMO_USERS. Wylogowanie i tak działa -# natychmiast, bo polega na skasowaniu ciasteczka. +# Bez własnego wolumenu nie ma gdzie trzymać licznika pokolenia sesji, więc +# „wyrzuć ze wszystkich urządzeń" robi się zmianą hasła w ASTRODEMO_USERS. +# Wylogowanie działa natychmiast, bo polega na skasowaniu ciasteczka. def _bezpieczne_dokad(dokad: str) -> str: """Adres powrotu przepuszczony przez sito — bez tego `?dokad=https://obcy.pl` @@ -207,4 +250,6 @@ def wyloguj(request: Request): @app.get("/health") def health() -> dict: - return {"status": "ok", "layer": "astroklient-demo"} + # Sama „ok". Nazwa warstwy mówiła, że warstw jest więcej i jak się nazywa; + # kubernetes i compose czytają wyłącznie kod odpowiedzi. + return {"status": "ok"} diff --git a/services/astroklient-demo/app/security.py b/services/astrodemo/app/security.py similarity index 88% rename from services/astroklient-demo/app/security.py rename to services/astrodemo/app/security.py index 00f16db..a5639b1 100644 --- a/services/astroklient-demo/app/security.py +++ b/services/astrodemo/app/security.py @@ -1,4 +1,4 @@ -"""Logowanie do astroklienta-demo (PRE-28). +"""Logowanie do astrodemo (PRE-28). OSOBNE KONTO, NIE WSPÓŁDZIELONE Z GŁÓWNĄ APLIKACJĄ. Demo pracuje na tej samej warstwie danych co produkcja, więc kto ma do niego dostęp, czyta oryginalne bazy @@ -6,7 +6,7 @@ interpretacyjne. Własny login i hasło pozwalają odciąć demo jedną zmienną środowiskową, bez ruszania kont głównej aplikacji i bez zmiany hasła komukolwiek. Mechanizm jest CELOWO prosty: jedno konto z konfiguracji, bez ekranu kont -i bez uprawnień. Astroklient ma dwie funkcje i obie są dla tego samego człowieka — +i bez uprawnień. Ta usługa ma dwie funkcje i obie są dla tego samego człowieka — model uprawnień z PRE-27 nie miałby tu czego dzielić. Hasło może być jawne albo jako hash scrypt (`scrypt$sól$hash`), tym samym formatem @@ -26,23 +26,27 @@ from collections import deque from fastapi import Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse -log = logging.getLogger("astroklient-demo.security") -audit_log = logging.getLogger("astroklient-demo.audit") +log = logging.getLogger("astrodemo.security") +audit_log = logging.getLogger("astrodemo.audit") _SCRYPT_N, _SCRYPT_R, _SCRYPT_P, _SCRYPT_LEN = 2**14, 8, 1, 32 _HASH_PREFIX = "scrypt$" # /logowanie i /wyloguj poza bramką: inaczej żeby się zalogować, trzeba by być # zalogowanym, a wyjście nie działałoby po wygaśnięciu sesji. -PUBLIC_PATHS = frozenset({"/health", "/logowanie", "/wyloguj"}) -PUBLIC_PREFIXES = ("/static/",) +PUBLIC_PATHS = frozenset({"/health", "/logowanie", "/wyloguj", "/static/styles.css"}) +# Pusto CELOWO. Prefiks "/static/" przepuszczał CAŁY katalog obok bramki: każdy +# plik pobierał ktokolwiek, także niezalogowany, i nie zostawiało to śladu ani +# w dzienniku, ani w limicie żądań. Publiczny jest jeden arkusz, wymieniony +# z nazwy, bo potrzebuje go ekran logowania. +PUBLIC_PREFIXES: tuple[str, ...] = () _hits: dict[str, deque[float]] = {} MAX_TRACKED_CLIENTS = 4096 def accounts() -> dict[str, str]: - """Konta demo: `login:sekret`, po przecinku, ze zmiennej DEMO_USERS. + """Konta demo: `login:sekret`, po przecinku, ze zmiennej ASTRODEMO_USERS. WIELE KONT, bo każde ma WŁASNĄ, niewidoczną dla pozostałych pulę baz (PRE-29). Wspólne konto oznaczałoby wspólną pulę, czyli klientów oglądających nawzajem @@ -51,10 +55,10 @@ def accounts() -> dict[str, str]: Sekret najlepiej jako hash scrypt (`scrypt$sól$hash`) — ten sam format i ten sam skrypt make_user.py, co w głównej aplikacji. - Zgodność wstecz: pojedyncze DEMO_USER/DEMO_PASSWORD działa, gdy DEMO_USERS + Zgodność wstecz: pojedyncze ASTRODEMO_USER/ASTRODEMO_PASSWORD działa, gdy ASTRODEMO_USERS jest puste.""" out: dict[str, str] = {} - for entry in os.getenv("DEMO_USERS", "").split(","): + for entry in os.getenv("ASTRODEMO_USERS", "").split(","): entry = entry.strip() if not entry: continue @@ -63,8 +67,8 @@ def accounts() -> dict[str, str]: out[user.strip()] = secret.strip() if out: return out - single = os.getenv("DEMO_PASSWORD", "") - return {os.getenv("DEMO_USER", "demo"): single} if single else {} + single = os.getenv("ASTRODEMO_PASSWORD", "") + return {os.getenv("ASTRODEMO_USER", "demo"): single} if single else {} def rate_limit_per_min() -> int: @@ -109,7 +113,7 @@ def issue_session(login: str) -> str: """Zawartość ciasteczka dla właśnie zalogowanego konta. W ciasteczku ląduje ODCISK poświadczenia, więc zmiana hasła albo usunięcie - konta z DEMO_USERS unieważnia jego sesje samo z siebie. To jest tu jedyny + konta z ASTRODEMO_USERS unieważnia jego sesje samo z siebie. To jest tu jedyny sposób zdalnego unieważnienia: demo nie ma gdzie trwale zapisywać stanu, bo nie ma własnego wolumenu — a stan sesji na udziale z bazami klientów byłby pomieszaniem dwóch zupełnie różnych rzeczy.""" @@ -217,7 +221,7 @@ def install(app) -> None: if not auth_enabled(): log.warning( - "UWAGA: brak kont (DEMO_USERS / DEMO_PASSWORD) — astroklient-demo stoi OTWARTY. " + "UWAGA: brak kont (ASTRODEMO_USERS / ASTRODEMO_PASSWORD) — astrodemo stoi OTWARTY. " "Bez logowania nie ma też PULI: wszyscy pracowaliby na jednym, wspólnym " "zbiorze baz, widząc nawzajem swoje wgrania." ) diff --git a/services/astroklient-demo/app/session.py b/services/astrodemo/app/session.py similarity index 96% rename from services/astroklient-demo/app/session.py rename to services/astrodemo/app/session.py index 47689d1..0ea7083 100644 --- a/services/astroklient-demo/app/session.py +++ b/services/astrodemo/app/session.py @@ -40,10 +40,10 @@ import json import os import time -# Własna nazwa, nie ta z pełnej aplikacji: gdyby obie stały kiedyś pod jedną -# domeną, ciasteczka o tej samej nazwie nadpisywałyby się nawzajem i człowiek -# wypadałby z jednej aplikacji, logując się do drugiej. -COOKIE = "astroklient_sesja" +# Nazwa własna tej usługi. Gdyby dwie aplikacje pod jedną domeną używały +# ciasteczka o tej samej nazwie, nadpisywałyby się nawzajem i człowiek wypadałby +# z jednej, logując się do drugiej. +COOKIE = "astrodemo_sesja" # Twardy kres życia sesji i kres bezczynności. Oba w sekundach, oba z konfiguracji. DEFAULT_MAX_AGE = 30 * 24 * 3600 # 30 dni — po tylu i tak każe się zalogować diff --git a/services/astroklient-demo/app/static/styles.css b/services/astrodemo/app/static/styles.css similarity index 92% rename from services/astroklient-demo/app/static/styles.css rename to services/astrodemo/app/static/styles.css index 71a6bd7..2e8699d 100644 --- a/services/astroklient-demo/app/static/styles.css +++ b/services/astrodemo/app/static/styles.css @@ -1,6 +1,5 @@ -/* astroklient-demo — arkusz CELOWO minimalny. Nie kopiujemy stylów pełnej aplikacji: - demo ma dwa formularze i tabelę wyników, a każda przeniesiona klasa niosłaby - nazwy funkcji, których tu nie ma. */ +/* Arkusz CELOWO minimalny: dwa formularze i tabela wyników. Każda klasa ponad to + niosłaby nazwę funkcji, której tu nie ma. */ :root { --bg: #11121b; --panel: #191b26; --line: #2b2e3d; --fg: #e8e9f0; --muted: #9aa0b4; --accent: #8ab4ff; diff --git a/services/astroklient-demo/app/templates/base.html b/services/astrodemo/app/templates/base.html similarity index 90% rename from services/astroklient-demo/app/templates/base.html rename to services/astrodemo/app/templates/base.html index ba0080f..3a54f58 100644 --- a/services/astroklient-demo/app/templates/base.html +++ b/services/astrodemo/app/templates/base.html @@ -3,13 +3,13 @@ - astroklient-demo · {% block title %}{% endblock %} + astrodemo · {% block title %}{% endblock %}
-

astroklient-demo

+

astrodemo

{# Kto jest zalogowany i wyjście. Demo bywa używane na cudzych komputerach, więc bez tego pierwszy klient zostawia otwartą sesję drugiemu. #} {% if auth_on() %} diff --git a/services/astroklient-demo/app/templates/index.html b/services/astrodemo/app/templates/index.html similarity index 100% rename from services/astroklient-demo/app/templates/index.html rename to services/astrodemo/app/templates/index.html diff --git a/services/astroklient-demo/app/templates/logowanie.html b/services/astrodemo/app/templates/logowanie.html similarity index 92% rename from services/astroklient-demo/app/templates/logowanie.html rename to services/astrodemo/app/templates/logowanie.html index cc41f83..6af2388 100644 --- a/services/astroklient-demo/app/templates/logowanie.html +++ b/services/astrodemo/app/templates/logowanie.html @@ -3,12 +3,12 @@ - astroklient-demo · logowanie + astrodemo · logowanie
-

astroklient-demo

+

astrodemo

{% if request.query_params.get('wylogowano') %}

Wylogowano.

diff --git a/services/astroklient-demo/requirements-dev.txt b/services/astrodemo/requirements-dev.txt similarity index 100% rename from services/astroklient-demo/requirements-dev.txt rename to services/astrodemo/requirements-dev.txt diff --git a/services/astroklient-demo/requirements.txt b/services/astrodemo/requirements.txt similarity index 100% rename from services/astroklient-demo/requirements.txt rename to services/astrodemo/requirements.txt diff --git a/services/astroklient-demo/tests/test_astroklient_demo.py b/services/astrodemo/tests/test_astrodemo.py similarity index 94% rename from services/astroklient-demo/tests/test_astroklient_demo.py rename to services/astrodemo/tests/test_astrodemo.py index f2a2f97..f5b4ea4 100644 --- a/services/astroklient-demo/tests/test_astroklient_demo.py +++ b/services/astrodemo/tests/test_astrodemo.py @@ -1,4 +1,4 @@ -"""astroklient-demo — wersja demonstracyjna (PRE-28). +"""astrodemo — wersja demonstracyjna (PRE-28). DWIE WŁASNOŚCI, KTÓRYCH PILNUJE TEN PLIK @@ -19,9 +19,9 @@ from app import security @pytest.fixture() def env(monkeypatch): - monkeypatch.setenv("DEMO_USERS", "demo:tajne-demo,klientA:hasloA,klientB:hasloB") - monkeypatch.delenv("DEMO_USER", raising=False) - monkeypatch.delenv("DEMO_PASSWORD", raising=False) + monkeypatch.setenv("ASTRODEMO_USERS", "demo:tajne-demo,klientA:hasloA,klientB:hasloB") + monkeypatch.delenv("ASTRODEMO_USER", raising=False) + monkeypatch.delenv("ASTRODEMO_PASSWORD", raising=False) monkeypatch.setenv("RATE_LIMIT_PER_MIN", "0") # Bez klucza podpisu usługa celowo nie wstaje (fail-closed, LOG-34). monkeypatch.setenv("SESSION_SECRET", "t" * 64) @@ -118,7 +118,7 @@ def test_without_credentials_nothing_is_served(env, monkeypatch): def test_demo_account_is_separate_from_the_main_application(env, monkeypatch): - """Konto astroklienta-demo MUSI być własne: demo pracuje na tej samej warstwie + """Konto astrodemo MUSI być własne: demo pracuje na tej samej warstwie danych co produkcja, więc odcięcie go nie może wymagać zmiany haseł w głównej aplikacji.""" monkeypatch.setenv("APP_USER", "astrololo") @@ -129,7 +129,7 @@ def test_demo_account_is_separate_from_the_main_application(env, monkeypatch): def test_a_password_with_polish_letters_is_refused_not_crashed(env, monkeypatch): - monkeypatch.setenv("DEMO_USERS", "demo:zażółć-gęślą") + monkeypatch.setenv("ASTRODEMO_USERS", "demo:zażółć-gęślą") c, _ = _client(monkeypatch) @@ -204,7 +204,11 @@ def test_a_broken_logic_layer_is_reported_not_crashed(env, monkeypatch): monkeypatch.setattr(LogicClient, "report", lambda self, **kw: (_ for _ in ()).throw( httpx.ConnectError("brak połączenia"))) r = c.post("/", headers=_auth(), data=FORM) - assert r.status_code == 200 and "Warstwa logiczna niedostępna" in r.text + # Komunikat jest CELOWO nieinformacyjny: treść wyjątku niosła nazwę usługi, + # jej port i nazwy tras, czyli mapę tego, co stoi za tym ekranem. + assert r.status_code == 200 + assert "Nie udało się teraz wykonać tej operacji" in r.text + assert "Warstwa logiczna" not in r.text and "8001" not in r.text def test_the_page_never_offers_anything_it_cannot_do(env, monkeypatch): @@ -280,12 +284,12 @@ def test_logging_in_and_out_of_the_demo(env, monkeypatch): def test_removing_an_account_kills_its_open_session(env, monkeypatch): """Demo nie ma gdzie trwale zapisywać stanu, więc zdalne unieważnienie robi - się przez DEMO_USERS — usunięcie albo zmiana hasła ubija sesję odciskiem.""" + się przez ASTRODEMO_USERS — usunięcie albo zmiana hasła ubija sesję odciskiem.""" c, _ = _client(monkeypatch) ciasteczko = _auth("klientA") assert c.get("/", headers=ciasteczko, follow_redirects=False).status_code == 200 - monkeypatch.setenv("DEMO_USERS", "demo:tajne-demo,klientB:hasloB") # bez klientA + monkeypatch.setenv("ASTRODEMO_USERS", "demo:tajne-demo,klientB:hasloB") # bez klientA assert c.get("/", headers=ciasteczko, follow_redirects=False).status_code == 303 diff --git a/services/astrodemo/tests/test_slownik_zakazany.py b/services/astrodemo/tests/test_slownik_zakazany.py new file mode 100644 index 0000000..19cc612 --- /dev/null +++ b/services/astrodemo/tests/test_slownik_zakazany.py @@ -0,0 +1,177 @@ +"""Zapora słownikowa astrodemo: czego użytkownik demo nie może zobaczyć. + +astrodemo stoi najniżej w drabinie trzech produktów (astrodemo → astroklient → +astrololo) i jest dawane najszerzej. Ma dwie funkcje i ma wyglądać, jakby to +było wszystko, co program potrafi. + +Test nie sprawdza pojedynczych miejsc, tylko przechodzi WSZYSTKO, co da się +pobrać, i szuka słów, które nie mają prawa paść. Sprawdza też DRZEWO PLIKÓW +usługi — bo „nie ma śladu w kodzie" znaczy, że `grep` po obrazie ma nic nie +znaleźć, a nie tylko że odpowiedź jest czysta. + +Powód, dla którego to powstało: komentarz w pierwszej linii styles.css mówił +„Nie kopiujemy stylów pełnej aplikacji", a arkusz pobierał anonim. +""" +import pathlib +import re + +import pytest + +APP = pathlib.Path(__file__).resolve().parents[1] / "app" + +# (a) że istnieje wersja obszerniejsza i jak się nazywa +WIEKSZY_BRAT = ["pełna aplikacja", "pełnej aplikacji", "pełna wersja", "pełnej wersji", + "astrololo", "astroklient", "wersji podstawowej", "upgrade", "rozszerzon"] +# (b) że tekst pisze model językowy +# Bez samego „token": w tej usłudze znaczy token sesji i token międzywarstwowy, +# a nie jednostkę rozliczeniową modelu. Zamiast tego zwroty jednoznaczne. +MODEL = ["LLM", "anthropic", "openai", "gpt", "claude", "prompt", + "generowanie tekstu", "sztuczn", "okno kontekstu", "model językow"] +# (c) funkcje, których astrodemo nie ma +INNE_FUNKCJE = ["kosmogram", "aspektarian", "synastri", "kalendarz", "predykcj", + "skompiluj", "system domów", "systemy domów", "deklinacj", "antyscj"] +# (d) że jest ktoś, kto widzi więcej, i że produkt jest w budowie +MECHANIZMY = ["administrator", "uprawnieni", "walidacj", "kwarantann", "roadmap", + "w budowie", "widok testowy", "warstwa logiczna", "layer"] +# Dwa poziomy, bo dwie różne drogi wycieku. +# +# WSZĘDZIE — także w kodzie serwera, którego przeglądarka nigdy nie zobaczy. +# Obraz tej usługi się KOMUŚ ODDAJE, więc kto go dostanie, przeczyta też +# komentarze i docstringi. Zdanie „w pełnej aplikacji to działa inaczej" mówi mu +# wprost, że istnieje pełna aplikacja — a to jest dokładnie to, czego ma nie +# wiedzieć. Komentarz ma tłumaczyć TĘ usługę, bez powoływania się na rodzeństwo. +WSZEDZIE = WIEKSZY_BRAT + MODEL + INNE_FUNKCJE +# TYLKO DO PRZEGLĄDARKI — słownictwo inżynierskie opisujące własne działanie tej +# usługi. W komentarzu serwera jest na miejscu; w odpowiedzi HTTP nie ma czego +# szukać, bo mówi o mechanizmach, o których użytkownik nie ma wiedzieć. +DO_PRZEGLADARKI = WSZEDZIE + MECHANIZMY +ZAKAZANE = DO_PRZEGLADARKI +WZORCE = [(r"(?i)\b(PRE|LOG|DAN)-\d+\b", "identyfikator wymagania"), + (r"(?i)\bfeat/[\w-]+", "nazwa gałęzi rozwojowej"), + (r"(?i)\bAI\b", "model językowy")] + + +def _grzechy(tresc: str, gdzie: str, slownik: list[str] | None = None) -> list[str]: + out = [] + for igla in (slownik if slownik is not None else ZAKAZANE): + if re.search(rf"(?i)(? bytes: if not frame.startswith(MAGIC): - raise LinkError("ramka bez znacznika astrololo") + raise LinkError("ramka bez znacznika protokołu") body = frame[len(MAGIC):] if len(body) <= NONCE_BYTES: raise LinkError("ramka za krótka") diff --git a/services/logic/app/link_crypto.py b/services/logic/app/link_crypto.py index 89ed3d8..da99bc2 100644 --- a/services/logic/app/link_crypto.py +++ b/services/logic/app/link_crypto.py @@ -153,7 +153,7 @@ class Link: def open(self, direction: bytes, path: str, stamp: str, seq: int, frame: bytes) -> bytes: if not frame.startswith(MAGIC): - raise LinkError("ramka bez znacznika astrololo") + raise LinkError("ramka bez znacznika protokołu") body = frame[len(MAGIC):] if len(body) <= NONCE_BYTES: raise LinkError("ramka za krótka") diff --git a/services/presentation/app/link_crypto.py b/services/presentation/app/link_crypto.py index 89ed3d8..da99bc2 100644 --- a/services/presentation/app/link_crypto.py +++ b/services/presentation/app/link_crypto.py @@ -153,7 +153,7 @@ class Link: def open(self, direction: bytes, path: str, stamp: str, seq: int, frame: bytes) -> bytes: if not frame.startswith(MAGIC): - raise LinkError("ramka bez znacznika astrololo") + raise LinkError("ramka bez znacznika protokołu") body = frame[len(MAGIC):] if len(body) <= NONCE_BYTES: raise LinkError("ramka za krótka") diff --git a/services/render/app/link_crypto.py b/services/render/app/link_crypto.py index 89ed3d8..da99bc2 100644 --- a/services/render/app/link_crypto.py +++ b/services/render/app/link_crypto.py @@ -153,7 +153,7 @@ class Link: def open(self, direction: bytes, path: str, stamp: str, seq: int, frame: bytes) -> bytes: if not frame.startswith(MAGIC): - raise LinkError("ramka bez znacznika astrololo") + raise LinkError("ramka bez znacznika protokołu") body = frame[len(MAGIC):] if len(body) <= NONCE_BYTES: raise LinkError("ramka za krótka")